387 lines
11 KiB
PHP
387 lines
11 KiB
PHP
<?php
|
|
/**
|
|
*
|
|
* @file
|
|
* @ingroup Extensions
|
|
* @author Bennet Bleßmann
|
|
* @copyright © 2021 Bennet Bleßmann
|
|
* @license GNU General Public Licence 2.0 or later
|
|
*/
|
|
|
|
if( !defined( 'MEDIAWIKI' ) ) {
|
|
echo( "This file is an extension to the MediaWiki software and cannot be used standalone.\n" );
|
|
die( 1 );
|
|
}
|
|
|
|
class TagEvents {
|
|
|
|
const XML_DECLARATION = '<?xml version="1.0" encoding="UTF-8" ?>';
|
|
|
|
public static function timezone() {
|
|
new DateTimeZone("Europe/Berlin");
|
|
}
|
|
|
|
static function eventsRenderer(?string $input, array $args, Parser $parser, PPFrame $frame) {
|
|
|
|
switch ($args["type"]) {
|
|
case 'source':
|
|
return self::renderSource($input, $args, $parser, $frame);
|
|
case 'reference':
|
|
return self::renderReference($input, $args, $parser, $frame);
|
|
default:
|
|
return "fsevents tag 'type' attribute should have either value 'source' or 'reference'";
|
|
}
|
|
|
|
}
|
|
|
|
|
|
static function renderReference(?string $input, array $args, Parser $parser, PPFrame $frame) {
|
|
|
|
if (!isset($args['source'])) {
|
|
return "source attribute not set";
|
|
}
|
|
|
|
$article_text = FSMod::loadArticle($args['source'], $parser);
|
|
|
|
$dcl = self::XML_DECLARATION;
|
|
|
|
$xml_content = <<<XML
|
|
{$dcl}
|
|
{$article_text}
|
|
XML;
|
|
|
|
libxml_use_internal_errors(true);
|
|
$xml = simplexml_load_string(trim($xml_content));
|
|
|
|
if ($xml === false) {
|
|
$errors = libxml_get_errors();
|
|
return FSMod::handle_xml_errors($errors, $input, $parser);
|
|
}
|
|
|
|
return self::renderEvents($xml, $input, $args, $parser, $frame);
|
|
}
|
|
|
|
|
|
static function renderSource(?string $input, array $args, Parser $parser, PPFrame $frame) {
|
|
|
|
$dcl = self::XML_DECLARATION;
|
|
|
|
$xml_content = <<<XML
|
|
{$dcl}
|
|
<fsevents>
|
|
{$input}
|
|
</fsevents>
|
|
XML;
|
|
|
|
libxml_use_internal_errors(true);
|
|
$xml = simplexml_load_string(trim($xml_content));
|
|
|
|
if ($xml === false) {
|
|
$errors = libxml_get_errors();
|
|
return FSMod::handle_xml_errors($errors, $input, $parser);
|
|
}
|
|
|
|
return self::renderEvents($xml, $input, $args, $parser, $frame);
|
|
}
|
|
|
|
static function renderEvents(SimpleXMLElement $xml, ?string $input, array $args, Parser $parser, PPFrame $frame) {
|
|
|
|
$events = $xml->xpath('event');
|
|
$exceptions = $xml->xpath('except');
|
|
|
|
$entries = self::processEventGroup(
|
|
$events,
|
|
$exceptions,
|
|
(int) ($args['limit'] ?? -1),
|
|
$input,
|
|
$args,
|
|
$parser,
|
|
$frame,
|
|
null,
|
|
null,
|
|
null
|
|
);
|
|
|
|
$output = "<table class='wikitable' style='float: right; width:1px;'><tbody>";
|
|
|
|
$output .= FSMod::parseTagRecursive($xml->head, $parser, $frame);
|
|
|
|
foreach ($entries as $entry) {
|
|
$output .= $entry->toString($parser, $frame);
|
|
}
|
|
|
|
$output .= "</tbody></table>";
|
|
|
|
return $output;
|
|
|
|
}
|
|
|
|
static function processEventGroup(
|
|
array $events,
|
|
array $exceptions,
|
|
int $limit = -1,
|
|
?string $input,
|
|
array $args,
|
|
Parser $parser,
|
|
PPFrame $frame,
|
|
?string $when,
|
|
?string $activity,
|
|
?string $place
|
|
) : array {
|
|
|
|
$entries = [];
|
|
|
|
foreach ( $events as $event) {
|
|
|
|
$instances = self::renderUpcomingEvent($event, $input, $args, $parser, $frame, $when, $activity, $place);
|
|
|
|
$entries = array_merge($entries, $instances);
|
|
|
|
}
|
|
|
|
$timezone = TagEvents::timezone();
|
|
|
|
$entries2 = array_filter($entries, function($event) use ($timezone, $exceptions){
|
|
foreach ( $exceptions as $exception ) {
|
|
|
|
$canceled = new DateTime($exception["date"], $timezone);
|
|
|
|
if ($canceled == $event->date) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
|
|
$entries = array_values($entries2);
|
|
|
|
# sort events based on theire datetime
|
|
uasort($entries, fn($event1, $event2) => $event1->sortkey <=> $event2->sortkey);
|
|
|
|
|
|
if ($limit > -1) {
|
|
return array_slice($entries, 0, $limit);
|
|
} else {
|
|
return $entries;
|
|
}
|
|
|
|
}
|
|
|
|
static function renderUpcomingEvent(
|
|
$event,
|
|
?string $input,
|
|
array $args,
|
|
Parser $parser,
|
|
PPFrame $frame,
|
|
?string $when,
|
|
?string $activity,
|
|
?string $place
|
|
) : array {
|
|
|
|
|
|
$timezone = TagEvents::timezone();
|
|
$today = new DateTime("today", $timezone);
|
|
|
|
if (isset($event->time)) {
|
|
$when = FSMod::parseTagRecursive($event->time, $parser, $frame);
|
|
}
|
|
|
|
if (isset($event->activity)) {
|
|
$activity = FSMod::parseTagRecursive($event->activity, $parser, $frame);
|
|
}
|
|
|
|
if (isset($event->place)) {
|
|
$place = FSMod::parseTagRecursive($event->place, $parser, $frame);
|
|
}
|
|
|
|
|
|
switch ($event['type']) {
|
|
case "single":
|
|
case "manual":
|
|
|
|
// the last day this event will be shown
|
|
$last_day = new DateTime($event['enddate'] ?? $event['date'], $timezone);
|
|
|
|
if ($last_day < $today) {
|
|
return [];
|
|
}
|
|
|
|
$date = new DateTime($event['date'], $timezone);
|
|
|
|
return [FSEvent::create_event($date, $event, $activity, $place, $when)];
|
|
|
|
case "group":
|
|
$child_events = $event->xpath('event');
|
|
$exceptions = $event->xpath('except');
|
|
|
|
return self::processEventGroup($child_events, $exceptions, (int) ($event['limit'] ?? -1), $input, $args, $parser, $frame, $when, $activity, $place);
|
|
|
|
case "weekly":
|
|
$count = (int)$event['count'];
|
|
|
|
$weekday = $event['weekday'];
|
|
$current = new DateTime($weekday);
|
|
$result = [];
|
|
|
|
while ( $count > count($result) ) {
|
|
$result[] = FSEvent::create_event(clone $current, $event, $activity, $place, $when);
|
|
$current = $current->modify("next {$weekday}");
|
|
}
|
|
return $result;
|
|
|
|
case "monthly-nth-weekday-plus":
|
|
$count = (int)$event['count'];
|
|
|
|
$ordinalday = $event['nth'] . " " . $event['weekday'] . " of";
|
|
$offset = ($event['offset'] ?? 0) . " days";
|
|
|
|
$current = (new DateTime($ordinalday))->modify($offset);
|
|
$result = [];
|
|
|
|
while ( $count > count($result) ) {
|
|
$now = clone $current;
|
|
if ($today <= $current) {
|
|
$result[] = FSEvent::create_event($now, $event, $activity, $place, $when);
|
|
}
|
|
|
|
// TODO handle case of first monday -1 could potentially loop,
|
|
// as negative offsets may move back a month again
|
|
$current = $current
|
|
->modify("next month")
|
|
->modify($ordinalday)
|
|
->modify($offset);
|
|
|
|
if ($now >= $current) {
|
|
// we are stuck or even moving backwards
|
|
break;
|
|
}
|
|
|
|
}
|
|
return $result;
|
|
}
|
|
}
|
|
}
|
|
|
|
class FSEvent {
|
|
|
|
public DateTime $sortkey;
|
|
|
|
// the start date for this event
|
|
// determins when to stop displaying an event in the absents of $this->end_date
|
|
public DateTime $date;
|
|
|
|
// the end date for this event (when different from start date)
|
|
// determins when to stop displaying an event when present
|
|
public ?DateTime $end_date;
|
|
|
|
// the start time for the event (when not complete day/unspecified)
|
|
public ?string $time;
|
|
// the end time for the event (when not open end/unspecified)
|
|
public ?string $end_time;
|
|
|
|
// override for automatic when field
|
|
public ?string $manual_datetime;
|
|
|
|
// what field content
|
|
public string $activity;
|
|
// where field content
|
|
public string $place;
|
|
|
|
function __construct(
|
|
DateTime $date,
|
|
string $activity,
|
|
string $place,
|
|
DateTime $end_date = null,
|
|
string $time = null,
|
|
string $end_time = null,
|
|
string $manual_datetime = null
|
|
) {
|
|
|
|
$this->date = $date;
|
|
$this->end_date = $end_date;
|
|
|
|
$this->time = $time;
|
|
$this->end_time = $end_time;
|
|
|
|
$this->sortkey = clone $date;
|
|
|
|
if (!is_null($this->time)) {
|
|
// time/starttime
|
|
$hourminute = explode(":", $this->time);
|
|
$this->sortkey->setTime((int)$hourminute[0],(int)$hourminute[1]);
|
|
}
|
|
|
|
$this->manual_datetime = $manual_datetime;
|
|
$this->activity = $activity;
|
|
$this->place = $place;
|
|
}
|
|
|
|
static function dotw(DateTime $datetime) {
|
|
// php starts with sunday but we start with monday
|
|
$dotw = (((int)$datetime->format("w")) + 6) % 7;
|
|
// day of the week Monday == 0 Sunday == 6
|
|
return $dotw;
|
|
}
|
|
|
|
static function create_event(DateTime $date, $event, ?string $activity, ?string $place , ?string $manual_time = null) {
|
|
$timezone = TagEvents::timezone();
|
|
|
|
$end_date = null;
|
|
if(isset($event['enddate'])) {
|
|
$end_date = new DateTime($event['enddate'], $timezone);
|
|
}
|
|
|
|
|
|
$time = $event['time'] ?? null;
|
|
$end_time = $event['endtime'] ?? null;
|
|
|
|
return new FSEvent($date, $activity ?? "No activity specified", $place ?? "No place specified", $end_date, $time, $end_time, $manual_time);
|
|
}
|
|
|
|
// build the string for the when field, used when $this->manual_datetime is absent
|
|
function autodate(Parser $parser, PPFrame $frame) : string {
|
|
$format = "d.m.Y";
|
|
$date = $this->date->format($format);
|
|
$weekday = self::dotw($this->date);
|
|
|
|
$whenday = "";
|
|
|
|
if (is_null($this->end_date)) {
|
|
$whenday = $parser->recursiveTagParse("{{Dict|word=day_{$weekday}}} {$date}", $frame);
|
|
} else {
|
|
$enddate = $this->end_date->format($format);
|
|
$endweekday = self::dotw($this->end_date);
|
|
|
|
$whenday = $parser->recursiveTagParse("{{Dict|word=day_{$weekday}}} {$date} - {{Dict|word=day_{$endweekday}}} {$enddate}", $frame);
|
|
}
|
|
|
|
$whentime = "";
|
|
|
|
if (!is_null($this->time)) {
|
|
if (is_null($this->end_time)) {
|
|
$whentime = $parser->recursiveTagParse("{$this->time} {{Dict|word=oclock}}", $frame);
|
|
} else {
|
|
$whentime = $parser->recursiveTagParse("{$this->time} {{Dict|word=oclock}} - {$this->end_time} {{Dict|word=oclock}}", $frame);
|
|
}
|
|
}
|
|
|
|
return trim($whenday . " " . $whentime);
|
|
}
|
|
|
|
public function toString(Parser $parser, PPFrame $frame): string {
|
|
$when = $this->manual_datetime ?? $this->autodate($parser, $frame);
|
|
$what = $this->activity;
|
|
$where = $this->place;
|
|
|
|
$render = <<<HTML
|
|
<tr>
|
|
<td>{$when}</td>
|
|
<td>{$what}</td>
|
|
<td>{$where}</td>
|
|
</tr>
|
|
HTML;
|
|
return $render;
|
|
}
|
|
}
|
|
|
|
|