Module documentation

The Module:Cronos is the core of the Meta:Cronos project and it's the Lua module implementing these templates:

See their related documentation for normal usage from wikitext.

Proceed in this page to discover how to read events using Cronos' public Lua APIs.

Overview edit

The source code of Module:Cronos is object-oriented and with lot of in-line documentation. Read-it.

In short there are some classes:

CronosEvent
whatever event (party, session, hackaton, etc.)
CronosDay
collector of events happening in a specific day
CronosCategory
one for each Event, this is its icon (see phab:T254586)
CronosCalendarContext
unuseful utility class used to store preferences and filters (e.g. Tags)

CronosDay API edit

You can access the data of a single Event from other Lua modules:

-- load the module
local Cronos = require( 'Module:Cronos' )

-- load a single day
local day = Cronos._day( '2019-03-25' )

-- load related events sorted by time
local events = day:getEventsSortedByTime()

-- loop these events
for i, event in pairs( events ) do

	-- examine start date
	mw.logObject( event:getStartDate() )

	-- examine end date
	mw.logObject( event:getEndDate() )

	-- examine its category icon
	mw.logObject( event:getCategory():getIconWikitext() )

	-- examine some other data available in the CronosEvent object
	mw.logObject( event )

end

CronosCategory API edit

You can also obtain other information like a known Category:

-- load the module
local Cronos = require( 'Module:Cronos' )

-- load a single category
local category = Cronos._category( 'libre' )

-- examine its icon
mw.logObject( category:getIconWikitext() )

-- examine its user identifier (e.g. 'libre')
mw.logObject( category.uid )

-- examine its name (e.g. 'Free Software initiatives')
mw.logObject( category.name )

-- examine some other data available in the CronosCategory object
mw.logObject( category )

Other APIs edit

Please look at the source code for everything exported in the p-ackage. The source code is intensively in-line documented.

Questions / Issues edit

For any question or idea feel free to write in the talk page pinging the current maintainer who is User:Valerio Bozzolan.

For any issue feel free to open a new Task in Wikimedia Phabricator with the #WMCH-Cronos Tag and assign it to valerio.bozzolan: Create a Task.

See also edit

---
-- 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.
--
-- This module does not have any dependency. Please keep this feature.
--
-- Note that this module needs to read some Event-related pages from your wiki.
-- This increases the "Expensive parser function count" by ~40. Anyway, the
-- global limit actually is 500.
--
-- Happy hacking!
--
-- @author [[User:Valerio Bozzolan]]
-- @creation 2019-04-13
-- @see https://phabricator.wikimedia.org/tag/wmch-cronos/
---

---
-- List of well-known categories
--
-- If you add a new Category please add a note in the talk page in order to
-- update the related website.
--
-- Note that the 'com' is expressed as default category in the configuration.
--
-- See:
--   https://phabricator.wikimedia.org/T254586
--
local CRONOS_CATEGORIES = {
	['com'] = { uid = 'com', name = 'Community initiatives', filename = 'File:Wikimedia Community Logo.svg' },
	['dat'] = { uid = 'dat', name = 'Wikidata initiatives', filename = 'File:Wikidata Favicon color.svg' },
	['edu'] = { uid = 'edu', name = 'Wikimedia Education Program', filename = 'File:WikipediaEduBelow.svg' },
	['libre'] = { uid = 'libre', name = 'Free Software and Open Source initiatives', filename = 'File:Heckert GNU white.svg' },
	['osm'] = { uid = 'osm', name = 'OpenStreetMap', filename = 'File:Openstreetmap logo.svg' },
	['glam'] = { uid = 'glam', name = 'Wikimedia GLAM Program', filename = 'File:GLAM logo.png' },
	['wmch'] = { uid = 'wmch', name = 'Wikimedia CH initiatives', filename = 'File:WikimediaCHLogo.svg' },
	['wbg'] = { uid = 'wbg', name = 'West Bengal Wikimedians initiatives', filename = 'File:Logo of West Bengal Wikimedians User Group.svg' },
	['wmf'] = { uid = 'wmf', name = 'Wikimedia Foundation initiatives', filename = 'File:Wikimedia-logo black.svg' },
	['wmno'] = { uid = 'wmno', name = 'Wikimedia Norge initiatives', filename = 'File:Wikimedia Norge-logo svart nb.svg' },
}

-- append a default dummy category
-- please keep this separated from the others for more visibility
CRONOS_CATEGORIES.default = { uid = 'default', name = 'Event', filename = 'File:Wikimedia Community Logo.svg' }

-- Default configuration
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. 'Meta:Cronos/Event/' for [[Meta:Cronos/Events/2000-12-31]
	['event_prefix'] = 'Meta:Cronos/Events/',

	-- Expected French Calendar dataset
	--  See [[Events calendar/events.json]] in Meta-Wikusa i
	--  Set to nil to disable this feature.
	['french_dataset_page'] = 'Events calendar/events.json',

	-- French Calendar home URL or page title
 	-- the French calendar has not a permalink for each event
	-- so these events just link to this homepage
	['french_dataset_url'] = 'Events calendar',

	-- If you have another cute web-Cronos frontend, put the URL here
	['cute_form_url'] = 'https://wmch.toolforge.org/cronos/',

	-- Default timezone
	--  '0': use local wiki timezone
	--  '1': use UTC timezone
	['utc'] = '0',

	-- Default start of the week
	--  '1': week start from Monday
	--  '0': week start from Sunday
	['start_from_monday'] = '1',

	-- How much weeks to be displayed in the calendar as default
	--  Note that if a week is splitted in two Months,
	--  you may get more lines in order to display them.
	['weeks'] = '4',

	-- How much months to shift in the past or in the future as default
	--   '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 it
	['max_title_len'] = '30',

	-- Choose to have a shorter week name as default
	['short_weekname'] = '0',

	-- Template used to briefly list some events
	-- As default: [[Template:Cronos day brief]]
	-- Arguments:
	--  yyyy year  in 4 digits
	--  mm   month in 2 digits
	--  dd   day   in 2 digits
	['template_day_brief'] = 'Cronos day brief',

	-- Template used to briefly tell something about an event
	-- As default: [[Template:Cronos event brief]]
	-- Arguments:
	--  yyyy
	--  mm
	--  dd
	--  title
	--  when
	--  end
	--  url
	--  editurl
	['template_event_brief'] = 'Cronos event brief',

	-- Template used to display an event in a single line
	-- As default: [[Template:Cronos event brief line]]
	-- Arguments:
	--  yyyy
	--  mm
	--  dd
	--  title
	--  when
	--  end
	--  url
	--  editurl
	['template_event_brief_line'] = 'Cronos event brief line',

	-- how much columns has the above template
	-- this is used to create the last line with some merged-columns
	-- with your tags
	['template_event_brief_line_columns'] = '5',

	-- Template used as header to briefly tell something about an event
	-- As default: [[Template:Cronos event brief line/Head]]
	['template_event_brief_line_head'] = 'Cronos event brief line/Head',

	-- Chips stylesheet
	-- Default to [[Template:Cronos event/chips.css]]
	['chips_stylesheet'] = 'Cronos event/chips.css',

	-- Default Category
	-- See https://phabricator.wikimedia.org/T254586
	['default_category'] = 'default',
}

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

-- this Lua package
local p = {}

-- constants
local SECONDS_IN_DAY = 86400;
local SECONDS_IN_MONTH = SECONDS_IN_DAY * 28 -- Not really :^) it's a lazy hack

-- template used for the heading of the calendar
-- default to [[Template:Cronos month/Head]]
local HEADING_TEMPLATE = 'Cronos month/Head'

-- template used to save/display data about a CronosEvent
-- default to [[Template:Cronos event]]
local EVENT_TEMPLATE = 'Cronos event'

-- this Lua pattern distinguish single events, and the known template arguments
-- name of the [[Template:Cronos event]]
local EVENT_PATTERN = '{{ *[Cc]ronos event'

--- Name of some expected {{Cronos event}} template arguments
-- title: Event title
-- when:  Event start time
-- tags:  Space separated list of Event tags
local EVENT_ARG_ID       = 'id'
local EVENT_ARG_TITLE    = 'title'
local EVENT_ARG_WHEN     = 'when'
local EVENT_ARG_TAGS     = 'tags'
local EVENT_ARG_URL      = 'url'
local EVENT_ARG_WHEN_END = 'end'
local EVENT_ARG_CATEGORY = 'category'
local EVENT_ARG_WHERE    = 'where'
local EVENT_ARG_ABSTRACT = 'abstract'

-- external identifiers
-- https://phabricator.wikimedia.org/T268213
local EVENT_ARG_ID_METAFR   = 'id meta-fr'
local EVENT_ARG_ID_WMF_PHAB = 'id wmf-phab'

-- all the damn external ids
local EVENT_ARG_EXTERNAL_IDS = {
	EVENT_ARG_ID_METAFR,
	EVENT_ARG_ID_WMF_PHAB,	
}

-- current date in raw format
-- to speed up the EventDay:isToday()
local TODAY_RAW = os.date( '%Y-%m-%d' )

---
-- BASE FUNCTIONS
---


---
-- Check if a value is inside an array
--
-- @param  string  needle   String to be searched
-- @param  string  haystack Array of strings
-- @return boolean True if the needle is in the string
local function inArray( needle, haystack )

	-- search the needle in the haystack
	for i, v in pairs( haystack ) do
		if needle == v then
			return true
		end
	end

	return false
end

---
-- Array merge
--
-- Merge the second array in the first one
--
-- @param table first
-- @param table second
local function arrayMerge( first, second )
	for k, v in pairs( second ) do
		first[ #first + 1 ] = v
	end
end

---
-- Array replace
--
-- This somehow emulates the PHP array_replace()
--
-- @param table..
-- @return table
--
local function arrayReplace( ... )

	-- final table
	local complete = {}

	-- table with all the arguments
	local args = { ... }

	-- for each table
	for _, arg in pairs( args ) do

		-- merge all the consecutive tables in the complete one
		for k, v in pairs( arg ) do

			-- the most left value takes precedence
			complete[ k ] = v
		end
	end

	return complete
end

--- Parse a single line of a template argument in wikitext
--
-- @param string s Wikitext
-- @param string|nil arg Argument name
local function parseTemplateArgument( s, arg )

	-- in Lua the '-' is a special character and should be escaped with '%'
	arg = string.gsub( 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 heading from wikitext
--
-- @param string s Wikitext
local function parseSectionHeading( 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

---
-- Remove empty tags from an array of Tags
--
-- If nothing interesting was found, it returns nil.
--
-- @param  tags table Array of strings
-- @return      table Array of strings without empty elements or nil if empty
local function arrayCleanTags( tags )

	local cleanArray = {}
	local founds = 0

	-- for each tags
	for i, v in pairs( tags ) do

		-- trim
		v = mw.text.trim( v )

		-- no value no party
		if v ~= '' then
			founds = founds + 1
			cleanArray[ founds ] = v
		end
	end

	-- no founds no party
	if founds == 0 then
		cleanArray = nil
	end

	return cleanArray
end

---
-- Remove empty elements from a table
--
-- @param  args table
-- @return table
local function cleanWikitextArguments( args )

	local result = {}

	-- clean each argument
	for k, v in pairs( args ) do

		-- eventually trim
		if type( v ) == 'string' then

			v = mw.text.trim( v )

			-- promote to nil
			if v == '' then
				v = nil	
			end
		end

		if v then
			result[ k ] = v
		end
	end

	return result
end

--- Parse some comma separated words
--
-- @param  string s Comma-separated tags
-- @return table Array of tags (without empty tags etc. or nil of no tag was OK)
local function parseTags( s )
	return arrayCleanTags( mw.text.split( s or '', ',', true ) )
end

---
-- Check if a string starts with something
--
-- @param string haystack
-- @param string needle
-- @return boolean
local function stringStartsWith( haystack, needle )
	local len = mw.ustring.len( needle ) 
	local firstPiece = mw.ustring.sub( haystack, 1, len )
	return needle == firstPiece
end

---
-- Check if something is a complete URL
--
-- @param string s Your string to be checked
-- @return boolean
local function isURL( s )
	return stringStartsWith( s, 'https://' )
	    or stringStartsWith( s, 'http://'  )
	    or stringStartsWith( s, '//'       ) -- [[rfc:3986]]
end

---
-- Adapt something to an URL
--
-- @param rawTitle Your page title or a full URL
-- @return A full URL
local function title2url( rawTitle )

	local url = nil

	-- check if this is already a good URL
	if isURL( rawTitle ) then
		url = rawTitle
	else
		-- check if it's a valid MediaWiki page title
		local title = mw.title.new( rawTitle )
		if title ~= nil then
			url = title:fullUrl()
		end
	end

	return url
end

---
-- Parse a raw date / datetime / time string and obtain a Lua date object
--
-- It accepts:
--  yyyy-mm-dd
--  yyyy-mm-dd HH:ii
--  HH:ii
--  Unix timestamp
--
-- @param  rawDateTime Raw date time in string format as 'YYYY-MNN-DD HH:ii' or just 'HH:ii' or Unix timestamp
-- @param  date        Optional object date useful to enrich the rawDateTime if it only consists in a time
-- @return Date object
local function parseDateOrTime( rawDateTime, date )

	local result

	local y, m, d, h, i

	-- maybe the result is a timestamp
	if type( rawDateTime ) == 'number' then
		return os.date( '*t', rawDateTime )
	end

	-- try to parse a complete date
	y, m, d, h, i = string.match(
		rawDateTime,
		'(%d%d%d%d)%-(%d%d)%-(%d%d) +(%d%d):(%d%d)'
	)

	-- otherwise try to parse just a time and inherit the date
	if not y then
		y = date.year
		m = date.month
		d = date.day
		h, i = string.match( rawDateTime, '(%d%d):(%d%d)' )
	end

	-- only return the time object if we was able to parse something
	if h then
		result = os.time{ year=y, month=m, day=d, hour=h, min=i }
		result = os.date( '*t', result )
	end

	return result
end

---
-- Render whatever as a time
--
-- If the input is a Unix timestamp, return 'HH:MM'
-- If the input is a complete date, return just the 'HH:MM'
-- If the input is a string, return a string.
--
-- @param rawTime Your time that can be a numeric timestamp or a 'HH:MM' string
-- @return string|nil
local function renderTime( rawTime )

	local time

	-- check the input type
	local type = type( rawTime )

	if type == 'number' then

		-- convert a unix timestamp to 'HH:MM'
		time = os.date( '%H:%M', rawTime )

	elseif type == 'table' then

		-- if it's a Lua date object, prints a time
		time = rawTime.hour .. ':' .. rawTime.min
	else

		-- this may be an 'HH:MM' or 'YYYY-mm-dd HH:ii:ss'
		hhii = string.match( rawTime, '%d%d%d%d%-%d%d%-%d%d +(%d%d:%d%d)' )
		if hhii ~= nil then
			time = hhii
		else
			-- just return the raw string otherwise
			time = rawTime
		end
	end

	return time
end

---
-- Index a single CronosEvent into a table of CronosEvent(s) indexed by raw date
--
-- It does not return anything.
-- It changes your 'groups' directly.
--
-- @param table Table of CronosEvent(s) indexed by raw date
-- @param table CronosEvent
local function indexCronosEventByDate( events, event )
	local rawDate = event.day:getRawDate()

	-- eventually create the group if it does not exist
	if events[ rawDate ] == nil then
		events[ rawDate ] = {}
	end

	-- append the CronosEvent in the group
	local group = events[ rawDate ]

	group[ #group + 1 ] = event
end

---
-- Index some CronosEvent(s) into a table of CronosEvent(s) indexed by raw date
--
-- It does not return anything.
-- It changes your 'groups' directly.
--
-- @param events Array of CronosEvent(s)
-- @return Associative array of CronosEvent(s) indexed by 'yyyy-mm-dd'
local function indexCronosEventsByDate( groups, events )
	for i, event in pairs( events ) do
		indexCronosEventByDate( groups, event )
	end
end

---
-- Group a collection of CronosEvent by date
--
-- @param events Array of CronosEvent(s)
-- @return Associative array of CronosEvent(s) indexed by 'yyyy-mm-dd'
local function groupCronosEventsByDate( events )
	local groups = {}
	indexCronosEventsByDate( groups, events )
	return groups
end

---
-- Enqueue a stylesheet
--
-- It creates a valid and generic <templatestyles src=""> tag.
--
-- See https://www.mediawiki.org/wiki/Extension:TemplateStyles	
--
-- @param title
-- @return string
local function templateStyles( title )
	return mw.getCurrentFrame():extensionTag{
		name = 'templatestyles',
		args = { src = title },
	}
end

---
-- Merge some frame arguments
--
-- @return table
local function frameArguments( frame )
	local argsParent = cleanWikitextArguments( frame:getParent().args or {} )
	local args       = cleanWikitextArguments( frame.args )
	return arrayReplace( args, argsParent )
end

---
-- Get a complete configuration inheriting default options
--
-- @param table|nil config
-- @return table
local function getConfig( config )
	return arrayReplace( DEFAULT_CONFIG, config )
end

---
-- Get a table of week names
--
-- @param  boolean short Set to true to have short week names
-- @return table
local function weekNamesLocalized( short )

	-- localized week names starting from Sunday
	local weekNames = {}
	for k, v in pairs( MW_WEEK_NAMES ) do

		-- localize the week name thanks to MediaWiki messages ([[MediaWiki:Monday]] etc.)
		v = mw.message.new( v ):plain()

		-- eventually short the week names
		if short then
			v = mw.ustring.sub( v, 1, 3 )
		end

		weekNames[ k ] = v
	end

	return weekNames
end

---
-- Get a list of week names starting from monday or sunday
--
-- @param table   week_names
-- @param boolean from_monday
-- @return table
local function weekNamesFrom( weekNames, fromMonday )
	local ordered = {}
	local i = fromMonday and 1 or 0
	local days = 1
	while days < 8 do
		ordered[ days ] = weekNames[ i % 7 + 1 ]
		i    = i    + 1
		days = days + 1
	end
	return ordered
end

---
-- Giving a number, print some empty cells
--
-- @param int cells
-- @return string
local function printEmptyColumns( cells )
	s = ''
	while cells > 0 do
		s = s .. '\n|'
		cells = cells - 1
	end
	return s
end

---
-- Compare two events by time
--
-- This method is useful for table.sort().
--
-- @param one Event
-- @param two Event
--
local function compareTwoEventsByTime( one, two )
	return one:getStartDateTimestamp()
	     < two:getStartDateTimestamp()
end


---
-- CLASSES
---

---
-- Simple class describing a single CronosEvent
---
local CronosEvent = {}
CronosEvent.__index = CronosEvent

---
-- Simple class collecting events
--
local CronosDay = {}
CronosDay.__index = CronosDay

---
-- Simple class organizing the Calendar
--
local CronosCalendarContext = {}
CronosCalendarContext.__index = CronosCalendarContext

---
-- Simple class organizing a CronosEvent's Category
--
local CronosCategory = {}
CronosCategory.__index = CronosCategory

-- all the Cronos Event(s) Categories indexed by UID
CronosCategory.all = {}

---
-- CLASS METHODS
---

---
-- Construct a clean CronosEvent object
--
-- @param event table Raw Event object to be incapsulated into a CronosEvent class.
--  Some of the supported arguments:
--
--  day      CronosDay object
--  when     Event start time hh:ii (or date and time yyyy-mm-dd hh:ii, or numeric UNIX timestamp)
--  whenEnd  Event   end time hh:ii (or date and time yyyy-mm-dd hh:ii, or numeric UNIX timestamp)
--  category Category identifier
--  tags     Table of Event tags
--  url      External URL
--  editurl  Edit URL (or default to Day's title)
--  context  Calendar context
--
-- @return CronosEvent
function CronosEvent:new( event )
	setmetatable( event, CronosEvent )

	-- the edit URL sometime can be guessed from the daily page title
	event.editurl = event.editurl or event.day.title

	return event
end

--- Construct 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 sectionTitle Title of the current section
-- @return table|nil
-- @return CronosEvent
function CronosEvent:createParsingDayBlock( day, wikitext, sectionTitle )

	local event = nil

	-- try to parse the Event title
	local title = parseTemplateArgument( wikitext, EVENT_ARG_TITLE ) or sectionTitle

	-- no title no party
	if title ~= nil then

		-- try to parse the Event start time
		local when = parseTemplateArgument( wikitext, EVENT_ARG_WHEN )

		-- no Event time no party
		if when ~= nil then

			-- allow empty arguments
			local args = {}

			-- try to parse the Event tags
			local tags = nil
			local tagsLine = parseTemplateArgument( wikitext, EVENT_ARG_TAGS )
			if tagsLine ~= nil then
				tags = parseTags( tagsLine )
			end

			-- try to parse the Event URL
			-- https://phabricator.wikimedia.org/T254160
			args.url = parseTemplateArgument( wikitext, EVENT_ARG_URL )

			-- try to parse the Event end date
			-- https://phabricator.wikimedia.org/T254333
			args.whenEnd = parseTemplateArgument( wikitext, EVENT_ARG_WHEN_END )

			-- try to parse the category
			-- https://phabricator.wikimedia.org/T254586
			args.category = parseTemplateArgument( wikitext, EVENT_ARG_CATEGORY )

			-- try to parse the location
			args.where = parseTemplateArgument( wikitext, EVENT_ARG_WHERE )

			-- try to parse the abstract
			args.abstract = parseTemplateArgument( wikitext, EVENT_ARG_ABSTRACT )

			-- create the Event object
			args.day   = day
			args.title = title
			args.when  = when
			args.tags  = tags

			-- mark this Event as local
			-- this distinguish a CronosEvent from an imported one
			-- https://phabricator.wikimedia.org/T268213
			args.source = 'local'

			-- check federated identifiers
			-- https://phabricator.wikimedia.org/T268213
			args.ids = {
				[ 'local'    ] = parseTemplateArgument( wikitext, EVENT_ARG_ID ),
				[ 'meta-fr'  ] = parseTemplateArgument( wikitext, EVENT_ARG_ID_METAFR ),
				[ 'wmf-phab' ] = parseTemplateArgument( wikitext, EVENT_ARG_ID_WMF_PHAB ),
			}

			-- create an Event with all the needed arguments
			event = CronosEvent:new( args )
		end
	end

	return event
end

---
-- Construct a CronosEvent from a French "Events calendar" data block
--
-- See https://meta.wikimedia.org/wiki/Events_calendar/events.json
-- See https://phabricator.wikimedia.org/T254264
--
-- @param data Single French Calendar event data block
-- @return CronosEvent
function CronosEvent:createFromFrenchData( calendarContext, data )

	-- Adapt French arguments
	-- note: their dates are not strings, but numeric UNIX timestamps

	-- initialize a dummy CronosDay for this event
	local day = CronosDay:createFromTimestamp( calendarContext, data.dtstart )

	-- external url
	local url = data.link and title2url( data.link )

	-- the French calendar has not a permalink for each event
	-- so just link to the home
	local editurl = DEFAULT_CONFIG[ 'french_dataset_url' ]

	-- return the CronosEvent object
	return CronosEvent:new( {
		day      = day,
		title    = data.title,
		when     = data.dtstart,
		whenEnd  = data.dtend,
		tags     = data.tags,
		where    = data.location,
		url      = url,
		editurl  = editurl,

		-- https://phabricator.wikimedia.org/T268213
		ids = {
			[ 'meta-fr' ] = data.id,
		},
		source  = 'meta-fr',
	} )
end

---
-- Get the start date as Lua date object
--
-- @return table Lua date object
function CronosEvent:getStartDate()
	return parseDateOrTime( self.when, self.day:getDate() )
end

---
-- Get the end date
--
-- If the end date is not specified, the default is the start date.
--
-- @return table Lua date object
function CronosEvent:getEndDate()
	return parseDateOrTime( self.whenEnd or self.when, self.day:getDate() )
end

---
-- Get the start date as Unix timestamp
--
-- @return int Unix timestamp
function CronosEvent:getStartDateTimestamp()
	return os.time( self:getStartDate() )
end

---
-- Get the end date as Unix timestamp
--
-- @return int Unix timestamp
function CronosEvent:getEndDateTimestamp()
	return os.time( self:getEndDate() )
end

---
-- Check if this Cronos event has a specific tag name
--
-- @param string tag Tag name
-- @See https://phabricator.wikimedia.org/T253074
-- @return boolean
--
function CronosEvent:hasTag( tag )
	return self.tags ~= nil and inArray( tag, self.tags )
end

---
-- Check if it exists an external identifier
--
-- @return boolean
--
function CronosEvent:hasExternalId()

	-- stop when reaching the first external id
	for i, argId in pairs( EVENT_ARG_EXTERNAL_IDS ) do
		if self.ids[ argId ] ~= nil then
			return true
		end
	end

	return false
end

---
-- Check if this CronosEvent is mirrored somewhere
--
-- @See https://phabricator.wikimedia.org/T268213
-- @return boolean
--
function CronosEvent:isMirror()
	return self.source == 'local' and self:hasExternalId()
end

---
-- Print the where location
--
-- @return string
function CronosEvent:getHumanWhere()
	local where = self.where
	if type( where ) == 'table' then
		where = mw.text.listToText( self.where )
	end
	return where
end

---
-- Check if this Cronos event has at least one of the specified Tags
--
-- @See https://phabricator.wikimedia.org/T253074
--
-- @param  object tags Array of tags
-- @return boolean True if one of the tags was associated to this Event
function CronosEvent:hasOneTag( tags )

	-- check if this Event as at least one of these Tags
	for i, tag in pairs( tags ) do
		if( self:hasTag( tag ) ) then
			return true
		end
	end

	-- not found
	return false
end

---
-- Return a table with a CronosEvent for each day of duration
--
-- When an event last
--
-- @return table
--
function CronosEvent:createEventsLastingMultipleDays()

	local events = {}

	-- if this has an original Event, it is a child
	-- expanding this will break everything
	if self.original == nil then

		local start = self:getStartDateTimestamp()
		local stop  = self:getEndDateTimestamp()
		local startHour = renderTime( start )
		local lastRawDate = self.day:getRawDate()
		local day = self.day
		local child

		-- well, the event may have no sense
		if start and stop then

			-- next day
			day = day:getNextDay()

			-- for each following day
			while day:getTimestamp() <= stop do

				-- produce a child
				child = {}
				for k, v in pairs( self ) do
					child[ k ] = v
				end

				-- set this new day but set a time (without full-date)
				child.day = day
				child.when = startHour

				-- remember my father
				child.original = self

				-- append this event
				events[ #events + 1 ] = CronosEvent:new( child )

				-- next day please
				day = day:getNextDay()
			end
		end
	end

	return events
end

---
-- Expand extra events lasting multiple days
--
function CronosEvent:expandEventsLastingMultipleDays()

	-- execute only once
	if self._expandedEventsLastingMultipleDays == nil then

		-- remember we have executed this
		self._expandedEventsLastingMultipleDays = self:createEventsLastingMultipleDays()

		-- publish each Event in its day
		for i, event in pairs( self._expandedEventsLastingMultipleDays ) do
			event:addToDay()
		end
	end

	return self._expandedEventsLastingMultipleDays
end

---
-- Get the duration of this Event in days
--
-- @return int
function CronosEvent:getDurationDays()
	return #self:expandEventsLastingMultipleDays()
end

---
-- Get a truncated title with eventually a tooltip
--
-- @return string
function CronosEvent:truncatedTitle()

	-- 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.context.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 title
end

---
-- Parameters useful for some Event templates
--
-- @return table
--
function CronosEvent:briefTemplateParameters()
	return {
		['yyyy']     = self.day.yyyy,
		['mm']       = self.day.mm,
		['dd']       = self.day.dd,
		['title']    = self:truncatedTitle(),
		['when']     = renderTime( self.when ),
		['end']      = self.whenEnd and renderTime( self.whenEnd ),
		['url']      = self.url and title2url( self.url ),
		['editurl']  = self.editurl,
		['category'] = self.category,
		['where']    = self:getHumanWhere(),
	}
end

---
-- Generate a brief abstract of this specific event
--
function CronosEvent:renderBriefCell( frame )
	frame = frame or mw.getCurrentFrame()

	-- this as default is [[Template:Cronos event brief]]
	return frame:expandTemplate{
		title = DEFAULT_CONFIG[ 'template_event_brief' ],
		args = self:briefTemplateParameters(),
	}	
end

---
-- Generate a brief line for this specific event
--
-- @return string
function CronosEvent:renderBriefLine()

	-- this as default is [[Template:Cronos event brief line]]
	return mw.getCurrentFrame():expandTemplate{
		title = DEFAULT_CONFIG[ 'template_event_brief_line' ],
		args = self:briefTemplateParameters(),
	}	
end

---
-- Get some information about the Category of this CronosEvent (if any)
--
-- It always return a Category. Always
--
-- See https://phabricator.wikimedia.org/T254586
--
-- @return table CronosCategory
function CronosEvent:getCategory()
	-- if the argument is not present assume a default one
	return CronosCategory.createFromUID( self.category )
end

---
-- Shortcut to add this CronosEvent to the related CronosDay
--
-- @param table CronosEvent
function CronosEvent:addToDay()
	self.day:addEvent( self )
end

---
-- Construct an empty CronosDay table
--
-- This entity was created to describe a 'Meta:Something/yyyy-mm-dd' page
-- but it also describe a generic date.
--
-- @param table context Optional Calendar Context
function CronosDay:new( context )

	-- instantiate a CronosDay object
	local day = {}
	setmetatable( day, CronosDay )

	-- keep the configuration as dependency injection
	day.context = context or CronosCalendarContext:new()

	-- initialize events
	day.events = {}

	return day
end

---
-- Construct a CronosDay table
--
-- This entity was created to describe a 'Meta:Something/yyyy-mm-dd' page
-- but it also describe a generic date.
--
-- @param date    Full date formatted in 'yyyy-mm-dd'
-- @param table   Calendar context
function CronosDay:createFromRawDate( date, calendarContext )

	-- initialize a new day with this configuration
	local day = CronosDay:new( calendarContext )

	-- remember the raw date formatted in yyyy-mm-dd
	day.rawdate = date

	-- extract single date members
	local dmy  = mw.text.split( date, '-', true )
	day.yyyy   = dmy[ 1 ]
	day.mm     = dmy[ 2 ]
	day.dd     = dmy[ 3 ]
	day.m      = tonumber( day.mm )

	day.timestamp = os.time( day:getDate() )
	day.week      = os.date( '%w', day.timestamp ) -- sunday is 0
	day.title     = calendarContext.config.event_prefix .. date

	-- assure to recycle an existing day
	return calendarContext:getUniqueDay( day )
end

---
-- Create a Cronos Day from a timestamp
--
-- @return CronosDay
--
function CronosDay:createFromTimestamp( calendarContext, timestamp )
	local day = CronosDay:new( calendarContext )

	-- basic information
	day.yyyy      = os.date( '%Y', timestamp ) -- full year
	day.mm        = os.date( '%m', timestamp ) -- full month 01-12
	day.dd        = os.date( '%d', timestamp ) -- full day   01-31
	day.week      = os.date( '%w', timestamp ) -- sunday is 0
	day.timestamp = timestamp

	-- assure to recycle an existing day
	return calendarContext:getUniqueDay( day )
end

---
-- Get a Lua date object indicating this date
--
-- @return object Luda date object
function CronosDay:getDate()
	return {
		year  = self.yyyy,
		month = self.mm,
		day   = self.dd,
	}
end

---
-- Get the date in format 'yyyy-mm-dd'
--
-- @return string
--
function CronosDay:getRawDate()

	-- eventually build it
	if self.rawdate == nil then
		self.rawdate = self.yyyy .. '-' .. self.mm .. '-' .. self.dd
	end

	return self.rawdate
end

---
-- Get the date as Unix timestamp
--
-- @return integer
--
function CronosDay:getTimestamp()
	return self.timestamp
end

---
-- Check if this CronosDay is today!
--
-- @return boolean
--
function CronosDay:isToday()
	return self:getRawDate() == TODAY_RAW
end

---
-- Get the wikitext of this event page
--
-- @return string|nil
function CronosDay:wikitext()

	local content = nil

	-- no title, no party
	if self.title then
		content = mw.title.new( self.title )
			:getContent()
	end

	return content
end

--- Add a CronosEvent to the collection
--
-- @param table CronosEvent
function CronosDay:addEvent( event )
	self.events[ #self.events + 1 ] = event
end

---
-- Parse this day page looking for events
--
function CronosDay:parse()

	-- save resources and parse only once
	if self._parsed == nil then

		-- this should be not initialized to do not overwrite some events
		-- self.events = {}

		-- shortcut
		local context = self.context

		-- requested Tags
		local filterTags = context.filters.tags or nil

		-- try to parse local events
		local wikitext = self:wikitext()
		if wikitext ~= nil then

			-- split the page in sections
			local sections = mw.text.split( wikitext, "\n=" )
			for _, section in pairs( sections ) do

				-- parse the section title
				local sectionTitle = parseSectionHeading( section )

				-- split the section content in event blocks
				local blocks = mw.text.split( section, EVENT_PATTERN )
				for i, block in pairs( blocks ) do

					-- parse each Event template
					local event = CronosEvent:createParsingDayBlock( self, block, sectionTitle )
					if event ~= nil then

						-- check if the Event matches filtersthe event must match filters
						-- and should not be a local mirror (to avoid duplicates)
						if context:canAccomodate( event ) and not event:isMirror() then
							event:addToDay()
						end
					end
				end
			end
		end

		-- find the French Calendar events related to this day
		local todayFrenchEvents = p._getFrenchCalendarEventsInDate( self.context, self:getRawDate() )
		if todayFrenchEvents ~= nil then
			for i, event in pairs( todayFrenchEvents ) do

				-- check if the Event matches filtersthe event must match filters
				-- and should not be a local mirror (to avoid duplicates)
				if context:canAccomodate( event ) and not event:isMirror() then
					event:addToDay()
				end
			end
		end

		-- mark this day as parsed
		self._parsed = true
	end
end

---
-- Get the CronosEvent(s) related to this day (if any)
--
-- @return CronosEvent[]
--
function CronosDay:getEvents()

	-- parse this Event
	-- this is safe to be called twice
	-- this populates self.events
	self:parse()

	-- expand Event(s) lasting multiple days
	-- this is safe to be called twice
	-- this populates self.events
	self:expandEventsLastingMultipleDays()

	-- table of CronosEvent(s)
	return self.events
end

---
-- Get the CronosEvent(s) related to this day (if any) ordered by time
--
-- @return CronosEvent[]
--
function CronosDay:getEventsSortedByTime()

	local events = self:getEvents()

	table.sort( events, compareTwoEventsByTime )

	return events
end

---
-- Get the CronosEvent identified with the provided source and id
--
-- If in this day there is not such event, it returns false.
--
-- @param string source Federation source identifier e.g. 'local' for 'this wiki'
-- @param string id
-- @return CronosEvent|false
--
function CronosDay:getEventBySourceId( source, id )

	-- find it by id
	for i, event in pairs( self:getEvents() ) do
		if event.ids[ source ] == id then
			return event
		end
	end

	return false
end

---
-- Shortcut to expand events lasting multiple days
--
function CronosDay:expandEventsLastingMultipleDays()
	for i, event in pairs( self.events ) do
		event:expandEventsLastingMultipleDays()
	end
end

---
-- Generate a brief header of this day
--
-- @return string
function CronosDay:renderBrief( frame )
	frame = frame or mw.getCurrentFrame()
	return frame:expandTemplate{
		title = DEFAULT_CONFIG[ 'template_day_brief' ],
		args = { self.yyyy, self.mm, self.dd },
	}
end

---
-- Generate a list of all the events in this day for a calendar cell
--
-- This builds the event list in a Calendar cell
--
-- @return string
function CronosDay:renderEventsForCell( frame )
	local s = ''
	frame = frame or mw.getCurrentFrame()
	for _, event in pairs( self:getEventsSortedByTime() ) do
		s = s .. "\n" .. event:renderBriefCell( frame )
	end
	return  s
end

---
-- Generate a calendar cell with a list of all the events in this day
--
-- @return string
function CronosDay:renderCalendarCell( frame )
	frame = frame or mw.getCurrentFrame()
	return self:renderBrief( frame )
	    .. self:renderEventsForCell( frame )
end

---
-- Get the next day
--
-- @return CronosDay
--
function CronosDay:getNextDay()
	return self.context:getDayFromTimestamp( self:getTimestamp() + SECONDS_IN_DAY )
end

---
-- Get the previous day
--
-- @return CronosDay
--
function CronosDay:getPreviusDay()
	return self.context:getDayFromTimestamp( self:getTimestamp() - SECONDS_IN_DAY )
end

---
-- Preload some days before this one
--
-- @param integer days Number of days to be preloaded
--
function CronosDay:preloadPreviusDays( days )
	local day
	local i = 1
	while i < days do
		day = CronosDay:getPreviusDay()

		-- this will trigger the cache
		day:getEvents()

		i = i + 1
	end
end

---
-- Constructor for a CronosEvent's Category
--
-- @param category Category arguments
--  Some of them:
--   uid:      string Category identifier
--   name:     string Category name
--   filename: string Category filename
--
-- See https://phabricator.wikimedia.org/T254586
--
function CronosCategory:new( category )
	setmetatable( category, CronosCategory )
	return category
end

---
-- Construct/find a CronosEvent's Category
--
-- It assures that you obtain the same category each time you call this
--
-- @param uid string Category UID like 'com' for 'community' or nil for the default
--
-- See https://phabricator.wikimedia.org/T254586
--
function CronosCategory.createFromUID( uid )

	local defaultUID = DEFAULT_CONFIG.default_category

	-- if the UID is missing assume the default category
	uid = uid or defaultUID

	-- check if this category was already generated
	if not CronosCategory.all[ uid ] then

		-- check if this category is known
		local categoryData = CRONOS_CATEGORIES[ uid ]
		if categoryData then

			-- instantiate this category for the first time
			CronosCategory.all[ uid ] = CronosCategory:new( categoryData )
		else

			-- in this case the Category is not known

			-- eventually throw an error if you have not the default
			if uid == defaultUID then
				error( 'whaat? missing default category with UID: ' .. defaultUID )
			end

			-- try with the default one
			-- the second parameter avoids any recursion
			-- that may happen if someone declares by mistake an
			-- unexisting default category
			return CronosCategory.createFromUID( defaultUID )
		end
	end

	-- just return the already existing Category instance
	return CronosCategory.all[ uid ]
end

---
-- Get the Cronos Event Category icon as wikitext
--
-- See https://phabricator.wikimedia.org/T254586
--
-- @param args Arguments
--  Accepted arguments:
--    size: string Size in pixels
--    link: string Link URL
-- @return string
function CronosCategory:getIconWikitext( args )
	local link = args and args.link or ''
	local size = args and args.size or '18px'
	return '[[' .. self.filename .. '|' .. size .. '|link=' .. link .. '|' .. self.name .. ']]'
end

---
-- Create a Calendar context
--
-- It returns some useful information acting as a middleware for the data needed
-- by all the available visualizations (calendar visualization and list).
--
-- @param  config Optional complete configuration
-- @return table
function CronosCalendarContext:new( config )

	-- instantiate a CronosCalendarContext object
	local calendarContext = {}
	setmetatable( calendarContext, CronosCalendarContext )

	-- expose the configuration
	calendarContext.config = getConfig( config or {} )

	-- localized week names starting from Sunday
	calendarContext.weekNames = weekNamesLocalized( calendarContext.config.short_weekname ~= '0' )

	-- number of week lines to be displayed
	calendarContext.weeks = tonumber( calendarContext.config.weeks )

	-- maximum number of days to be displayed
	-- do not confuse with the 'dayList' attribute
	calendarContext.days = calendarContext.weeks * 7

	-- start from Monday or from Sunday
	calendarContext.startFromMonday = calendarContext.config.start_from_monday == '1'

	-- start the week names from the correct day (Monday or Sunday)
	calendarContext.weekNamesFrom = weekNamesFrom( calendarContext.weekNames, calendarContext.startFromMonday )

	-- the user may want to filter by some factors
	calendarContext.filters = {
		tags = nil,
	}

	-- eventually filter by some Tags
	if calendarContext.config.tags ~= nil then
		calendarContext.filters.tags = parseTags( calendarContext.config.tags )
	end

	-- shift the current time month forward or backward by some months
	calendarContext.monthShift = tonumber( calendarContext.config.month_shift )

	-- unix time starting from now (eventually shifting)
	calendarContext.startWeekTime = os.time() + calendarContext.monthShift * SECONDS_IN_MONTH

	-- build the format date
	-- see the documentation of os.date() about UTC or local time
	local format_prefix = ''
	if calendarContext.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
	calendarContext.today = os.date( format_date, calendarContext.startWeekTime )

	-- prepare week indexes
	calendarContext.weekStart = 1
	calendarContext.weekEnd   = 8
	if calendarContext.startFromMonday then
		calendarContext.weekStart = calendarContext.weekStart + 1
		calendarContext.weekEnd   = calendarContext.weekEnd   + 1
	end

	-- how much days since the start of this week
	-- note that the week day starts from sunday = 1, monday = 2, etc.
	--   so as default if it's Monday should be considered "1 days" since start of week
	--   so as default if it's Sunday should be considered "0 days" since start of week
	calendarContext.daysSinceStartWeek = calendarContext.today.wday - 1

	-- eventually consider Monday as start of the week
	--   so as default if it's Monday should be considered "0 days" since start of week
	--   so as default if it's Sunday should be considered "6 days" since start of week
	if calendarContext.startFromMonday then
		calendarContext.daysSinceStartWeek = calendarContext.daysSinceStartWeek - 1
		if calendarContext.daysSinceStartWeek == -1 then
			calendarContext.daysSinceStartWeek = 6
		end
	end

	-- CronosDay(s) indexed by raw date
	calendarContext.dayByDate = {}

	return calendarContext
end

---
-- Check if the current context "can accomodate" the current Event
--
-- In short it checks if the Event matches current filters (Tags etc.)
--
-- @param event table Event
-- @return boolean
--
function CronosCalendarContext:canAccomodate( event )

	-- eventually filter by Tag
	-- https://phabricator.wikimedia.org/T253074
	local tags = self.filters.tags
	if tags ~= nil and not event:hasOneTag( tags ) then
		return false
	end

	-- as default the event matches the context
	return true
end

---
-- Get an unique day
--
-- This function assures that your day is unique for your calendar context.
-- In this way you do not create multiple different days rappresenting the same day.
--
-- @param day CronosDay
-- @return CronosDay
function CronosCalendarContext:getUniqueDay( day )

	-- the date in 'yyyy-mm-dd'
	local rawDate = day:getRawDate()

	-- eventually initialize the singleton
	if self.dayByDate[ rawDate ] == nil then
		self.dayByDate[ rawDate ] = day
	end

	-- returns the singleton
	return self.dayByDate[ rawDate ]
end

---
-- Shortcut to get a CronosDay from a raw date
--
-- @param string rawDate
-- @return CronosDay
function CronosCalendarContext:getDayFromRawDate( rawDate )
	return CronosDay:createFromRawDate( rawDate, self )
end

---
-- Shortcut to get a CronosDay from a Unix timestamp
--
-- @param integer timestamp
-- @return CronosDay
function CronosCalendarContext:getDayFromTimestamp( timestamp )
	return CronosDay:createFromTimestamp( self, timestamp )
end

---
-- Get a table of empty days starting from this week
--
-- @return table Table of CronosDay(s)
function CronosCalendarContext:getDays()

	-- list of days to be returned
	local dayList = {}

	-- calculate the date when this week started
	local seconds_since_start_week = self.daysSinceStartWeek * SECONDS_IN_DAY
	local time_start_of_week = self.startWeekTime - seconds_since_start_week

	-- see the documentation of os.date() about UTC or local time
	local format_prefix = ''
	if self.config.utc == '1' then
		format_prefix = '!'
	end

	-- os.date() formats to obtain a date table or a date in Y-m-d
	local format_ymd  = format_prefix .. '%F'

	-- generate the list of the days
	local i = 0
	while i < self.days do

		-- prepare the CronosDay object
		local event_time = time_start_of_week + i * SECONDS_IN_DAY
		local event_ymd  = os.date( format_ymd, event_time ) 

		-- add this well-known CronosDay to the list
		i = i + 1
		dayList[ i ] = self:getDayFromRawDate( event_ymd )
	end

	return dayList
end

---
-- Read the French Calendar dataset (raw format)
--
-- You should not call this twice without any kind of cache.
--
-- See https://meta.wikimedia.org/wiki/Events_calendar/events.json
-- See https://phabricator.wikimedia.org/T254264
--
-- @return Table of raw events
function p._parseFrenchCalendarDatasetRawContent()
	-- load the page
	local page = mw.title.new( DEFAULT_CONFIG.french_dataset_page )

	-- load the JSON
	local content = page:getContent()

    -- @TODO: replace with mw.loadJsonData() when will be available (after 18 October 2022)
	-- return an array of events
	return mw.text.jsonDecode( content )
end


---
-- ENDPOINTS
---

---
-- Convert a title or an URL... to an URL
--
-- This is intended to be called from Wikitext.
--
-- @return string
--
function p.title2url( frame )
	local args = frame.args

	-- take the first argument from the parent template of from the direct one
	local titleOrURL = args[1]

	return titleOrURL and title2url( titleOrURL )
end

---
-- Create a CalendarContext object
--
-- This is intended to be called from Lua.
--
-- @param table config Optional configuration
-- @return CalendarContext
function p._createCalendarContext( config )
	return CronosCalendarContext:new( config )
end

---
-- Read the French Calendar dataset and convert them to CronosEvent(s)
--
-- This is intended to be called from Lua.
--
-- Note that this method does not have any kind of cache.
--
-- @param table Calendar Context
-- @return Table of events
function p._parseFrenchCalendarCronosEvents( calendarContext )

	-- events to be returned
	local events = {}

	-- increment the expensive function call another time
	-- parsing all of this stuff should be considered expensive
	-- because they are so much:
	--  june 2020: 505 (!)
	mw.incrementExpensiveFunctionCount()

	-- for each raw event
	for i, rawEvent in ipairs( p._parseFrenchCalendarDatasetRawContent() ) do

		-- append this structured event
		events[ i ] = CronosEvent:createFromFrenchData( calendarContext, rawEvent )
	end

	return events
end

---
-- Get the French Calendar Dataset grouped by date
--
-- This function uses a cache to allow multiple calls.
--
-- See https://meta.wikimedia.org/wiki/Events_calendar/events.json
-- See https://phabricator.wikimedia.org/T254264
--
-- @param table Calendar context
function p._getFrenchCalendarEventsGroupedByDate( calendarContext )
	-- assure that this is initialized only once
	if calendarContext.frenchEventsByDate == nil then
		calendarContext.frenchEventsByDate = groupCronosEventsByDate( p._parseFrenchCalendarCronosEvents( calendarContext ) )
	end
	return calendarContext.frenchEventsByDate
end

---
-- Get the French Calendar Dataset of a single day
--
-- If nothing is present it returns nil.
--
-- See https://meta.wikimedia.org/wiki/Events_calendar/events.json
-- See https://phabricator.wikimedia.org/T254264
--
-- @param object Calendar context
-- @param date Raw date formatted as 'yyyy-mm-dd'
-- @return table or nil
function p._getFrenchCalendarEventsInDate( context, rawDate )
	local eventsByDate = p._getFrenchCalendarEventsGroupedByDate( context )
	return eventsByDate[ rawDate ]
end

---
-- Show some Tag "chips"
--
-- This should be used from Lua and not from wikitext.
--
-- @param  table  Tags
-- @return string Wikitext
--
function p._tagChips( tags )

	-- create a chip for each Tag
	local chips = {}
	for i, tag in pairs( tags ) do
		chips[ i ] = '<span class="cronos-tag-chip">' .. tag .. '</span>'
	end

	-- include the stylesheet for the Tag chips
	local s = templateStyles( DEFAULT_CONFIG[ 'chips_stylesheet' ] )

	-- append stylesheet and tags
	s = s .. table.concat( chips, ' ' )

	return s
end

---
-- Show some Tag "chips" (from wikitext)
--
-- This should be used from wikitext and not from Lua.
--
-- @return string Wikitext
--
function p.tagChips( frame )

	-- merge parent args etc.
	local args = frameArguments( frame )

	-- take the first argument from the parent template of from the direct one
	local tagsRaw = args[1] or args.tags

	-- parse the tags
	local tags = parseTags( tagsRaw )

	-- create the chips
	return tags and p._tagChips( tags )
end

---
-- Get a CronosCategory object from its UID
--
-- This should be used from Lua and not from wikitext.
--
-- @param string uid Category UID
-- @return CronosCategory
function p._category( uid )
	return CronosCategory.createFromUID( uid )
end

---
-- Print a CronosCategory icon from its UID
--
-- This should be used from wikitext and not from Lua.
--
-- @param string uid Category UID
function p.categoryIcon( frame )
	local args = frameArguments( frame )
	local uid = args[1] or args.uid
	return p._category( uid ):getIconWikitext( {
		link = args.link,
		size = args.size,
	} )
end

---
-- Print a CronosCategory name from its UID
--
-- This should be used from wikitext and not from Lua.
--
-- @param string uid Category UID
function p.categoryName( frame )
	local args = frameArguments( frame )
	local uid = args[1] or args.uid
	return p._category( uid ).name
end

---
-- Get an URL to add an event
--
-- This method should be called from Lua and not from wikitext
--
-- @param table args Query string arguments
-- @return string
function p._getAddEventURL( args )

	---
	-- As default this is something like:
	--   'https://wmch.toolforge.org/cronos/'
	local url = DEFAULT_CONFIG[ 'cute_form_url' ]

	-- eventually append the query string, if any
	if args ~= nil then
		local queryString = mw.uri.buildQueryString( args )
		if queryString ~= '' then
			url = url .. '?' .. queryString
		end
	end

	return url
end

---
-- Get an URL to add an event
--
-- This method should be called from Wikitext and not from Lua
--
-- @param frame
-- @return string
function p.getAddEventURL( frame )
	local args = frameArguments( frame )
	local query = {}

	-- eventually append tags
	local tags = args['tags']
	if tags ~= nil then
		local tagsTable = parseTags( tags )
		if #tagsTable > 0 then
			query.tags = table.concat( tagsTable, ',' )
		end
	end
	
	return p._getAddEventURL( query )
end

---
-- Generate the monthly calendar (Lua API)
--
-- This should be used from Lua and not from wikitext.
--
-- @param  object userConfig
-- @return string
function p._main( userConfig )

	-- output string
	local s = ''

	-- get everything I need
	local calendarContext = CronosCalendarContext:new( userConfig )

	-- local configuration
	local config = calendarContext.config

	-- frame of the current page
	local frame = mw.getCurrentFrame()

	-- append heading template
	s = s .. frame:expandTemplate{ title = HEADING_TEMPLATE }

	-- append start of the table
	s = s .. '\n{| class="wikitable cronos-month-table"'

	-- list of days
	local dayList = calendarContext:getDays()

	-- generate the table body
	local lastMonth = -1
	for i, day in pairs( dayList ) do

		-- print a newline?
		local newline = false

		-- domain: 0-6
		local column = ( i - 1 ) % 7

		if day.m == lastMonth then
			-- new row
			if column == 0 then
				s = s .. '\n|-\n'
			end
		else
			-- print week labels
			lastMonth = day.m

			-- print a new row if the month is changed
			if column > 0 then
				s = s .. printEmptyColumns( 7 - column )
				s = s .. '\n|-\n'
			end

			-- add row with month name
			local monthName = mw.message.new( MW_MONTH_NAMES[ lastMonth ] ):plain()

			-- eventually separate the month name from the above lines
			if i ~= 1 then
				s = s .. '\n|-\n'
			end
			s = s .. '\n!colspan="7" class="cronos-month-name"|' .. monthName
			s = s .. '\n|-\n'

			-- add row with week names
			for k, v in pairs( calendarContext.weekNamesFrom ) do
				s = s .. '\n!' .. v
			end
			s = s .. '\n|-\n'

			-- eventually print some empty columns
			if column > 0 then
				s = s .. printEmptyColumns( column )
			end
		end

		-- allow to style in a different way the current day cell
		if day:isToday() then
			s = s .. '\n|class="cronos-day-current"|'
		else
			s = s .. '\n|'
		end

		-- put the day in the cell
		s = s .. day:renderCalendarCell( frame )

		-- next day
		i = i + 1
	end

	-- eventually cite our filters
	if calendarContext.filters.tags ~= nil then
		s = s .. '\n|-\n'
		s = s .. '\n|colspan="7|' .. p._tagChips( calendarContext.filters.tags )
	end

	return s .. '\n|}'
end

---
-- Try to obtain all the used Tags
--
-- The Tags will be returned as an array of objects like:
--  {
--    { tag = 'foo', count = 2 },
--    { tag = 'bar', count = 1 },
--    ...
-- }
--
--
-- @see https://phabricator.wikimedia.org/T276350
-- @return table
--
function p._tags( userConfig )

	-- output
	local tags = {}

	-- get everything I need
	local calendarContext = CronosCalendarContext:new( userConfig )

	-- for each Day
	for i, day in pairs( calendarContext:getDays() ) do

		-- for each Event
		for k, event in pairs( day:getEvents() ) do

			-- for each Tag
			if event.tags ~= nil then
				for j, tag in pairs( event.tags ) do

					-- eventually initialize
					if tags[ tag ] == nil then

						tags[ tag ] = {
							tag   = tag,
							count = 0
						}

					end

					-- increase count
					tags[ tag ].count = tags[ tag ].count + 1
				end
			end
		end
	end


	return tags
end

---
-- Generate a cute Tag Cloud
--
-- [[phab:T276666]]
-- https://phabricator.wikimedia.org/T276666
--
-- This should be used from wikitext and not from Lua.
--
function p.tagCloud( frame )

	-- see [[Module:TagCloud]]
	local moduleTagCloud = require( 'Module:TagCloud' )

	-- parse user config
	local userConfig = frameArguments( frame )

	-- find the Tags in use in the calendar
	local tags = p._tags( userConfig )

	-- print the tag cloud
	return moduleTagCloud._main( {
		tags = tags,
	} )
end

---
-- Generate the monthly calendar
--
-- This should be used from Lua and not from wikitext.
--
-- @param  object userConfig Optional configuration
-- @return string
--
-- @see https://phabricator.wikimedia.org/T262016
function p._list( userConfig )

	-- output string
	local s = ''

	-- get everything I need
	local calendarContext = CronosCalendarContext:new( userConfig )

	-- prepared configuration
	local config = calendarContext.config

	-- frame of the current page
	local frame = mw.getCurrentFrame()

	-- columns of the table
	local columns = config.template_event_brief_line_columns

	-- append heading template with stylesheet
	s = s .. frame:expandTemplate{ title = HEADING_TEMPLATE }

	-- append table header
	-- as default [[Template:Event brief line/Head]]
	s = s .. '\n'
	s = s .. frame:expandTemplate{ title = config.template_event_brief_line_head }

	-- days involved in this list
	local dayList = calendarContext:getDays()

	-- generate the table body
	local lastMonth = -1
	local todayEvents
	for i, day in pairs( dayList ) do

		-- events related to this day
		todayEvents = day:getEventsSortedByTime()

		-- plot every single event of this day
		for _, event in pairs( todayEvents ) do

			-- new row
			s = s .. '\n|-\n'

			-- render the event line (some cells)
			s = s .. event:renderBriefLine()
		end

		-- next day
		i = i + 1
	end

	-- eventually cite our filters
	if calendarContext.filters.tags ~= nil then
		s = s .. '\n|-'
		s = s .. '\n|colspan="' .. columns .. '"|' .. p._tagChips( calendarContext.filters.tags )
	end

	return s .. '\n|}'
end

---
-- Generate the monthly calendar
--
-- This should be used from wikitext and not from Lua.
--
function p.main( frame )
	local args = frameArguments( frame )
	return p._main( args )
end

---
-- Generate the list calendar
--
-- This should be used from wikitext and not from Lua.
--
-- See https://phabricator.wikimedia.org/T262016
--
function p.list( frame )
	local args = frameArguments( frame )
	return p._list( args )
end

---
-- Get a single CronosDay object
--
-- This should be used from Lua and not from wikitext.
--
-- You can use this in your Lua console e.g.:
--    =mw.logObject( p._day( '2019-03-25' ) )
--
-- @param date   Date formatted as 'yyyy-mm-dd'
-- @param config Optional configuration
--
function p._day( date, config )

	-- no date no party
	if not date then
		error( 'missing date' )
	end

	-- create a Calendar context
	local calendarContext = CronosCalendarContext:new( config )

	-- create from the raw date
	return CronosDay:createFromRawDate( date, calendarContext )
end

---
-- Print the events of a single CronosDay
--
-- This should be used from wikitext and not from Lua.
--
-- @param object config
--
function p.day( frame )
	local args = frameArguments( frame )
	local date = frame.args[ 1 ]
	local day = p._day( date, frame.args )
	return day:renderEvents( frame )
end

---
-- Get a single CronosEvent object from a date and an ID
--
-- The date is very important because there is not a central database,
-- so we start from a day, and then we filter its events by the provided ID
-- to keep this earch reliable and scalable and also to prevent any kind of
-- potential duplicates from external sources.
--
-- This should be used from Lua and not from wikitext.
--
-- You can use this in your Lua console e.g.:
--    = mw.logObject( p._event( {
--       date = '2019-03-25',
--       source = 'local',
--       id = 'cronos-2019-03-25-asd1',
--     ) )
--
-- @param args   table Arguents. Some of them:
--   date   (string formatted as 'yyyy-mm-dd')
--   source (string like 'local')
--   id     (string like '123-asd')
-- @param config Optional configuration
--
function p._event( args )

	local date = args.date

	-- create a generic day
	local day = p._day( args.date, args.config )

	-- no source no party
	if not args.source then
		error( 'missing source' )
	end

	-- no id no party
	if not args.id then
		error( 'missing id' )
	end

	-- return the event or nil
	return day:getEventBySourceId( args.source, args.id )
end

---
-- Display a single Event
--
-- The date is very important because there is not a central database,
-- so we start from a day, and then we filter its events by the provided ID
-- to keep this earch reliable and scalable and also to prevent any kind of
-- potential duplicates from external sources.
--
-- This should be used from wikitext and not from Lua.
--
-- Parameters:
--   date   (string formatted as 'yyyy-mm-dd')
--   source (string like 'local')
--   id     (string like '123-asd')
--
-- @param date   Date formatted as 'yyyy-mm-dd'
-- @param id     Event identifier as 'cronos-asd'
-- @param config Optional configuration
--
function p.event( frame )

	local args = frameArguments( frame )
	local date    = args.date
	local source  = args.source
	local id      = args.id
	local config  = nil

	-- find the event by this id
	local event = p._event{
		date   = date,
		source = source,
		id     = id,
		config = config,
	}

	local tagList = ''
	if event.tags ~= nil then
		tagList = table.concat( event.tags, ', ' )
	end

	-- fill the arguments for the template
	local args = {
		[ EVENT_ARG_TITLE    ] = event.title,
		[ EVENT_ARG_WHERE    ] = event.where,
		[ EVENT_ARG_WHEN     ] = event.when,
		[ EVENT_ARG_WHEN_END ] = event.whenEnd,
		[ EVENT_ARG_TAGS     ] = tagList,
		[ EVENT_ARG_URL      ] = event.url,
		[ EVENT_ARG_CATEGORY ] = event.category,
		[ EVENT_ARG_ABSTRACT ] = event.abstract,
		[ EVENT_ARG_ID       ] = event.ids.id,
	}

	-- pass each external identifiers
	for i, argId in pairs( EVENT_ARG_EXTERNAL_IDS ) do
		args[ argId ] = event.ids[ argId ]
	end

	return frame:expandTemplate{ title = EVENT_TEMPLATE, args  = args }
end

return p