FS-Mod/FSMod/TagEvents.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;
}
}