Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); } public function handle($match, $state, $pos, Doku_Handler $handler) { $isEventList = (strpos($match, '{{eventlist') === 0); $isEventPanel = (strpos($match, '{{eventpanel') === 0); if ($isEventList) { $match = substr($match, 12, -2); } elseif ($isEventPanel) { $match = substr($match, 13, -2); } else { $match = substr($match, 10, -2); } $params = array( 'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'), 'year' => date('Y'), 'month' => date('n'), 'namespace' => '', 'daterange' => '', 'date' => '' ); if (trim($match)) { $pairs = preg_split('/\s+/', trim($match)); foreach ($pairs as $pair) { if (strpos($pair, '=') !== false) { list($key, $value) = explode('=', $pair, 2); $params[trim($key)] = trim($value); } } } return $params; } public function render($mode, Doku_Renderer $renderer, $data) { if ($mode !== 'xhtml') return false; if ($data['type'] === 'eventlist') { $html = $this->renderStandaloneEventList($data); } elseif ($data['type'] === 'eventpanel') { $html = $this->renderEventPanelOnly($data); } else { $html = $this->renderCompactCalendar($data); } $renderer->doc .= $html; return true; } private function renderCompactCalendar($data) { $year = (int)$data['year']; $month = (int)$data['month']; $namespace = $data['namespace']; $events = $this->loadEvents($namespace, $year, $month); $calId = 'cal_' . md5(serialize($data) . microtime()); $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); $prevMonth = $month - 1; $prevYear = $year; if ($prevMonth < 1) { $prevMonth = 12; $prevYear--; } $nextMonth = $month + 1; $nextYear = $year; if ($nextMonth > 12) { $nextMonth = 1; $nextYear++; } $html = '
'; // Embed events data as JSON for JavaScript access $html .= ''; // Left side: Calendar $html .= '
'; // Header with navigation $html .= '
'; $html .= ''; $html .= '

' . $monthName . '

'; $html .= ''; $html .= '
'; // Calendar grid $html .= ''; $html .= ''; $html .= ''; $html .= ''; $firstDay = mktime(0, 0, 0, $month, 1, $year); $daysInMonth = date('t', $firstDay); $dayOfWeek = date('w', $firstDay); $currentDay = 1; $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7); for ($row = 0; $row < $rowCount; $row++) { $html .= ''; for ($col = 0; $col < 7; $col++) { if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) { $html .= ''; } else { $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay); $isToday = ($dateKey === date('Y-m-d')); $hasEvents = isset($events[$dateKey]) && !empty($events[$dateKey]); $classes = 'cal-day'; if ($isToday) $classes .= ' cal-today'; if ($hasEvents) $classes .= ' cal-has-events'; $html .= ''; $currentDay++; } } $html .= ''; } $html .= '
SMTWTFS
'; $html .= '' . $currentDay . ''; if ($hasEvents) { // Sort events by time (no time first, then by time) $sortedEvents = $events[$dateKey]; usort($sortedEvents, function($a, $b) { $timeA = isset($a['time']) ? $a['time'] : ''; $timeB = isset($b['time']) ? $b['time'] : ''; // Events without time go first if (empty($timeA) && !empty($timeB)) return -1; if (!empty($timeA) && empty($timeB)) return 1; if (empty($timeA) && empty($timeB)) return 0; // Sort by time return strcmp($timeA, $timeB); }); // Show colored stacked bars for each event $html .= '
'; foreach ($sortedEvents as $evt) { $eventId = isset($evt['id']) ? $evt['id'] : ''; $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db'; $eventTime = isset($evt['time']) ? $evt['time'] : ''; $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event'; $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed'; $html .= ''; $html .= ''; } $html .= '
'; } $html .= '
'; $html .= '
'; // End calendar-left // Right side: Event list $html .= '
'; $html .= '
'; $html .= '
'; $html .= '

Events

'; if ($namespace) { $html .= '' . htmlspecialchars($namespace) . ''; } $html .= '
'; $html .= ''; $html .= '
'; $html .= '
'; $html .= $this->renderEventListContent($events, $calId, $namespace); $html .= '
'; $html .= '
'; // End calendar-right // Event dialog $html .= $this->renderEventDialog($calId, $namespace); $html .= '
'; // End container return $html; } private function renderEventListContent($events, $calId, $namespace) { if (empty($events)) { return '

No events this month

'; } $html = ''; ksort($events); foreach ($events as $dateKey => $dayEvents) { foreach ($dayEvents as $event) { $eventId = isset($event['id']) ? $event['id'] : ''; $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; $time = isset($event['time']) ? htmlspecialchars($event['time']) : ''; $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; $description = isset($event['description']) ? $event['description'] : ''; $isTask = isset($event['isTask']) ? $event['isTask'] : false; $completed = isset($event['completed']) ? $event['completed'] : false; $endDate = isset($event['endDate']) ? $event['endDate'] : ''; // Process description for wiki syntax, HTML, images, and links $renderedDescription = $this->renderDescription($description); // Convert to 12-hour format $displayTime = ''; if ($time) { $timeObj = DateTime::createFromFormat('H:i', $time); if ($timeObj) { $displayTime = $timeObj->format('g:i A'); } else { $displayTime = $time; } } // Format date display $dateObj = new DateTime($dateKey); $displayDate = $dateObj->format('M j'); // Multi-day indicator $multiDay = ''; if ($endDate && $endDate !== $dateKey) { $endObj = new DateTime($endDate); $multiDay = ' → ' . $endObj->format('M j'); } $completedClass = $completed ? ' event-completed' : ''; $html .= '
'; $html .= '
'; $html .= '
'; $html .= '' . $title . ''; $html .= '
'; $html .= '
'; $html .= '' . $displayDate . $multiDay; if ($displayTime) { $html .= ' • ' . $displayTime; } $html .= ''; $html .= '
'; if ($description) { $html .= '
' . $renderedDescription . '
'; } $html .= '
'; // event-info $html .= '
'; $html .= ''; $html .= ''; $html .= '
'; // Checkbox for tasks - ON THE FAR RIGHT if ($isTask) { $checked = $completed ? 'checked' : ''; $html .= ''; } $html .= '
'; } } return $html; } private function renderEventPanelOnly($data) { $year = (int)$data['year']; $month = (int)$data['month']; $namespace = $data['namespace']; $events = $this->loadEvents($namespace, $year, $month); $calId = 'panel_' . md5(serialize($data) . microtime()); $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); $prevMonth = $month - 1; $prevYear = $year; if ($prevMonth < 1) { $prevMonth = 12; $prevYear--; } $nextMonth = $month + 1; $nextYear = $year; if ($nextMonth > 12) { $nextMonth = 1; $nextYear++; } $html = '
'; // Header with navigation $html .= '
'; $html .= ''; $html .= '

' . $monthName . ' Events

'; $html .= ''; $html .= '
'; $html .= '
'; $html .= ''; $html .= '
'; $html .= '
'; $html .= $this->renderEventListContent($events, $calId, $namespace); $html .= '
'; $html .= $this->renderEventDialog($calId, $namespace); $html .= '
'; return $html; } private function renderStandaloneEventList($data) { $namespace = $data['namespace']; $daterange = $data['daterange']; $date = $data['date']; if ($daterange) { list($startDate, $endDate) = explode(':', $daterange); } elseif ($date) { $startDate = $date; $endDate = $date; } else { $startDate = date('Y-m-01'); $endDate = date('Y-m-t'); } $allEvents = array(); $start = new DateTime($startDate); $end = new DateTime($endDate); $end->modify('+1 day'); $interval = new DateInterval('P1D'); $period = new DatePeriod($start, $interval, $end); static $loadedMonths = array(); foreach ($period as $dt) { $year = (int)$dt->format('Y'); $month = (int)$dt->format('n'); $dateKey = $dt->format('Y-m-d'); $monthKey = $year . '-' . $month; if (!isset($loadedMonths[$monthKey])) { $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); } $monthEvents = $loadedMonths[$monthKey]; if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { $allEvents[$dateKey] = $monthEvents[$dateKey]; } } $html = '
'; $html .= '

Events: ' . date('M j', strtotime($startDate)) . ' - ' . date('M j, Y', strtotime($endDate)) . '

'; if (empty($allEvents)) { $html .= '

No events in this date range

'; } else { foreach ($allEvents as $dateKey => $dayEvents) { $displayDate = date('l, F j, Y', strtotime($dateKey)); $html .= '
'; $html .= '

' . $displayDate . '

'; foreach ($dayEvents as $event) { $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; $time = isset($event['time']) ? htmlspecialchars($event['time']) : ''; $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; $description = isset($event['description']) ? htmlspecialchars($event['description']) : ''; $html .= '
'; $html .= '
'; $html .= '
'; if ($time) { $html .= '' . $time . ''; } $html .= '' . $title . ''; if ($description) { $html .= '
' . nl2br($description) . '
'; } $html .= '
'; } $html .= '
'; } } $html .= '
'; return $html; } private function renderEventDialog($calId, $namespace) { $html = ''; return $html; } private function renderDescription($description) { if (empty($description)) { return ''; } // Convert newlines to
for basic formatting $rendered = nl2br($description); // Convert DokuWiki image syntax {{image.jpg}} to HTML $rendered = preg_replace_callback( '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/', function($matches) { $imagePath = trim($matches[1]); $alt = isset($matches[2]) ? trim($matches[2]) : ''; // Handle external URLs (http:// or https://) if (preg_match('/^https?:\/\//', $imagePath)) { return '' . htmlspecialchars($alt) . ''; } // Handle internal DokuWiki images $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath); return '' . htmlspecialchars($alt) . ''; }, $rendered ); // Convert DokuWiki link syntax [[link|text]] to HTML $rendered = preg_replace_callback( '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/', function($matches) { $link = trim($matches[1]); $text = isset($matches[2]) ? trim($matches[2]) : $link; // Handle external URLs if (preg_match('/^https?:\/\//', $link)) { return '' . htmlspecialchars($text) . ''; } // Handle internal DokuWiki links $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($link); return '' . htmlspecialchars($text) . ''; }, $rendered ); // Convert markdown-style links [text](url) to HTML $rendered = preg_replace_callback( '/\[([^\]]+)\]\(([^)]+)\)/', function($matches) { $text = trim($matches[1]); $url = trim($matches[2]); if (preg_match('/^https?:\/\//', $url)) { return '' . htmlspecialchars($text) . ''; } return '' . htmlspecialchars($text) . ''; }, $rendered ); // Convert plain URLs to clickable links $rendered = preg_replace_callback( '/(https?:\/\/[^\s<]+)/', function($matches) { $url = $matches[1]; return '' . htmlspecialchars($url) . ''; }, $rendered ); // Allow basic HTML tags (bold, italic, strong, em, u, code) // Already in the description, just pass through return $rendered; } private function loadEvents($namespace, $year, $month) { $dataDir = DOKU_INC . 'data/meta/'; if ($namespace) { $dataDir .= str_replace(':', '/', $namespace) . '/'; } $dataDir .= 'calendar/'; $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); if (file_exists($eventFile)) { $json = file_get_contents($eventFile); return json_decode($json, true); } return array(); } }