Module:Cronos

From Meta, a Wikimedia project coordination wiki
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