119378907SAtari911<?php 219378907SAtari911/** 319378907SAtari911 * DokuWiki Plugin calendar (Syntax Component) 419378907SAtari911 * Compact design with integrated event list 519378907SAtari911 * 619378907SAtari911 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 719378907SAtari911 * @author DokuWiki Community 819378907SAtari911 */ 919378907SAtari911 1019378907SAtari911if (!defined('DOKU_INC')) die(); 1119378907SAtari911 1219378907SAtari911class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin { 1319378907SAtari911 1419378907SAtari911 public function getType() { 1519378907SAtari911 return 'substition'; 1619378907SAtari911 } 1719378907SAtari911 1819378907SAtari911 public function getPType() { 1919378907SAtari911 return 'block'; 2019378907SAtari911 } 2119378907SAtari911 2219378907SAtari911 public function getSort() { 2319378907SAtari911 return 155; 2419378907SAtari911 } 2519378907SAtari911 2619378907SAtari911 public function connectTo($mode) { 2719378907SAtari911 $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 2819378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 2919378907SAtari911 $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar'); 3019378907SAtari911 } 3119378907SAtari911 3219378907SAtari911 public function handle($match, $state, $pos, Doku_Handler $handler) { 3319378907SAtari911 $isEventList = (strpos($match, '{{eventlist') === 0); 3419378907SAtari911 $isEventPanel = (strpos($match, '{{eventpanel') === 0); 3519378907SAtari911 3619378907SAtari911 if ($isEventList) { 3719378907SAtari911 $match = substr($match, 12, -2); 3819378907SAtari911 } elseif ($isEventPanel) { 3919378907SAtari911 $match = substr($match, 13, -2); 4019378907SAtari911 } else { 4119378907SAtari911 $match = substr($match, 10, -2); 4219378907SAtari911 } 4319378907SAtari911 4419378907SAtari911 $params = array( 4519378907SAtari911 'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'), 4619378907SAtari911 'year' => date('Y'), 4719378907SAtari911 'month' => date('n'), 4819378907SAtari911 'namespace' => '', 4919378907SAtari911 'daterange' => '', 5019378907SAtari911 'date' => '' 5119378907SAtari911 ); 5219378907SAtari911 5319378907SAtari911 if (trim($match)) { 5419378907SAtari911 $pairs = preg_split('/\s+/', trim($match)); 5519378907SAtari911 foreach ($pairs as $pair) { 5619378907SAtari911 if (strpos($pair, '=') !== false) { 5719378907SAtari911 list($key, $value) = explode('=', $pair, 2); 5819378907SAtari911 $params[trim($key)] = trim($value); 59*87ac9bf3SAtari911 } else { 60*87ac9bf3SAtari911 // Handle standalone flags like "today" 61*87ac9bf3SAtari911 $params[trim($pair)] = true; 6219378907SAtari911 } 6319378907SAtari911 } 6419378907SAtari911 } 6519378907SAtari911 6619378907SAtari911 return $params; 6719378907SAtari911 } 6819378907SAtari911 6919378907SAtari911 public function render($mode, Doku_Renderer $renderer, $data) { 7019378907SAtari911 if ($mode !== 'xhtml') return false; 7119378907SAtari911 7219378907SAtari911 if ($data['type'] === 'eventlist') { 7319378907SAtari911 $html = $this->renderStandaloneEventList($data); 7419378907SAtari911 } elseif ($data['type'] === 'eventpanel') { 7519378907SAtari911 $html = $this->renderEventPanelOnly($data); 7619378907SAtari911 } else { 7719378907SAtari911 $html = $this->renderCompactCalendar($data); 7819378907SAtari911 } 7919378907SAtari911 8019378907SAtari911 $renderer->doc .= $html; 8119378907SAtari911 return true; 8219378907SAtari911 } 8319378907SAtari911 8419378907SAtari911 private function renderCompactCalendar($data) { 8519378907SAtari911 $year = (int)$data['year']; 8619378907SAtari911 $month = (int)$data['month']; 8719378907SAtari911 $namespace = $data['namespace']; 8819378907SAtari911 8919378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 9019378907SAtari911 $calId = 'cal_' . md5(serialize($data) . microtime()); 9119378907SAtari911 9219378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 9319378907SAtari911 9419378907SAtari911 $prevMonth = $month - 1; 9519378907SAtari911 $prevYear = $year; 9619378907SAtari911 if ($prevMonth < 1) { 9719378907SAtari911 $prevMonth = 12; 9819378907SAtari911 $prevYear--; 9919378907SAtari911 } 10019378907SAtari911 10119378907SAtari911 $nextMonth = $month + 1; 10219378907SAtari911 $nextYear = $year; 10319378907SAtari911 if ($nextMonth > 12) { 10419378907SAtari911 $nextMonth = 1; 10519378907SAtari911 $nextYear++; 10619378907SAtari911 } 10719378907SAtari911 10819378907SAtari911 $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">'; 10919378907SAtari911 11019378907SAtari911 // Embed events data as JSON for JavaScript access 11119378907SAtari911 $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>'; 11219378907SAtari911 11319378907SAtari911 // Left side: Calendar 11419378907SAtari911 $html .= '<div class="calendar-compact-left">'; 11519378907SAtari911 11619378907SAtari911 // Header with navigation 11719378907SAtari911 $html .= '<div class="calendar-compact-header">'; 11819378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 119*87ac9bf3SAtari911 $html .= '<h3 class="calendar-month-picker" onclick="openMonthPicker(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . '</h3>'; 12019378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 121*87ac9bf3SAtari911 $html .= '<button class="cal-today-btn" onclick="jumpToToday(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 12219378907SAtari911 $html .= '</div>'; 12319378907SAtari911 12419378907SAtari911 // Calendar grid 12519378907SAtari911 $html .= '<table class="calendar-compact-grid">'; 12619378907SAtari911 $html .= '<thead><tr>'; 12719378907SAtari911 $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>'; 12819378907SAtari911 $html .= '</tr></thead><tbody>'; 12919378907SAtari911 13019378907SAtari911 $firstDay = mktime(0, 0, 0, $month, 1, $year); 13119378907SAtari911 $daysInMonth = date('t', $firstDay); 13219378907SAtari911 $dayOfWeek = date('w', $firstDay); 13319378907SAtari911 134*87ac9bf3SAtari911 // Load events from previous and next months to catch spanning events 135*87ac9bf3SAtari911 $prevMonth = $month - 1; 136*87ac9bf3SAtari911 $prevYear = $year; 137*87ac9bf3SAtari911 if ($prevMonth < 1) { 138*87ac9bf3SAtari911 $prevMonth = 12; 139*87ac9bf3SAtari911 $prevYear--; 140*87ac9bf3SAtari911 } 141*87ac9bf3SAtari911 142*87ac9bf3SAtari911 $nextMonth = $month + 1; 143*87ac9bf3SAtari911 $nextYear = $year; 144*87ac9bf3SAtari911 if ($nextMonth > 12) { 145*87ac9bf3SAtari911 $nextMonth = 1; 146*87ac9bf3SAtari911 $nextYear++; 147*87ac9bf3SAtari911 } 148*87ac9bf3SAtari911 149*87ac9bf3SAtari911 $prevMonthEvents = $this->loadEvents($namespace, $prevYear, $prevMonth); 150*87ac9bf3SAtari911 $nextMonthEvents = $this->loadEvents($namespace, $nextYear, $nextMonth); 151*87ac9bf3SAtari911 152*87ac9bf3SAtari911 // Combine all events for processing 153*87ac9bf3SAtari911 $allEvents = array_merge($events, $prevMonthEvents, $nextMonthEvents); 154*87ac9bf3SAtari911 155*87ac9bf3SAtari911 // Build a map of all events with their date ranges 156*87ac9bf3SAtari911 $eventRanges = array(); 157*87ac9bf3SAtari911 foreach ($allEvents as $dateKey => $dayEvents) { 158*87ac9bf3SAtari911 foreach ($dayEvents as $evt) { 159*87ac9bf3SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 160*87ac9bf3SAtari911 $startDate = $dateKey; 161*87ac9bf3SAtari911 $endDate = isset($evt['endDate']) && $evt['endDate'] ? $evt['endDate'] : $dateKey; 162*87ac9bf3SAtari911 163*87ac9bf3SAtari911 // Only process events that touch this month 164*87ac9bf3SAtari911 $eventStart = new DateTime($startDate); 165*87ac9bf3SAtari911 $eventEnd = new DateTime($endDate); 166*87ac9bf3SAtari911 $monthStart = new DateTime(sprintf('%04d-%02d-01', $year, $month)); 167*87ac9bf3SAtari911 $monthEnd = new DateTime(sprintf('%04d-%02d-%02d', $year, $month, $daysInMonth)); 168*87ac9bf3SAtari911 169*87ac9bf3SAtari911 // Skip if event doesn't overlap with current month 170*87ac9bf3SAtari911 if ($eventEnd < $monthStart || $eventStart > $monthEnd) { 171*87ac9bf3SAtari911 continue; 172*87ac9bf3SAtari911 } 173*87ac9bf3SAtari911 174*87ac9bf3SAtari911 // Create entry for each day the event spans 175*87ac9bf3SAtari911 $current = clone $eventStart; 176*87ac9bf3SAtari911 while ($current <= $eventEnd) { 177*87ac9bf3SAtari911 $currentKey = $current->format('Y-m-d'); 178*87ac9bf3SAtari911 179*87ac9bf3SAtari911 // Check if this date is in current month 180*87ac9bf3SAtari911 $currentDate = DateTime::createFromFormat('Y-m-d', $currentKey); 181*87ac9bf3SAtari911 if ($currentDate && $currentDate->format('Y-m') === sprintf('%04d-%02d', $year, $month)) { 182*87ac9bf3SAtari911 if (!isset($eventRanges[$currentKey])) { 183*87ac9bf3SAtari911 $eventRanges[$currentKey] = array(); 184*87ac9bf3SAtari911 } 185*87ac9bf3SAtari911 186*87ac9bf3SAtari911 // Add event with span information 187*87ac9bf3SAtari911 $evt['_span_start'] = $startDate; 188*87ac9bf3SAtari911 $evt['_span_end'] = $endDate; 189*87ac9bf3SAtari911 $evt['_is_first_day'] = ($currentKey === $startDate); 190*87ac9bf3SAtari911 $evt['_is_last_day'] = ($currentKey === $endDate); 191*87ac9bf3SAtari911 $evt['_original_date'] = $dateKey; // Keep track of original date 192*87ac9bf3SAtari911 193*87ac9bf3SAtari911 // Check if event continues from previous month or to next month 194*87ac9bf3SAtari911 $evt['_continues_from_prev'] = ($eventStart < $monthStart); 195*87ac9bf3SAtari911 $evt['_continues_to_next'] = ($eventEnd > $monthEnd); 196*87ac9bf3SAtari911 197*87ac9bf3SAtari911 $eventRanges[$currentKey][] = $evt; 198*87ac9bf3SAtari911 } 199*87ac9bf3SAtari911 200*87ac9bf3SAtari911 $current->modify('+1 day'); 201*87ac9bf3SAtari911 } 202*87ac9bf3SAtari911 } 203*87ac9bf3SAtari911 } 204*87ac9bf3SAtari911 20519378907SAtari911 $currentDay = 1; 20619378907SAtari911 $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7); 20719378907SAtari911 20819378907SAtari911 for ($row = 0; $row < $rowCount; $row++) { 20919378907SAtari911 $html .= '<tr>'; 21019378907SAtari911 for ($col = 0; $col < 7; $col++) { 21119378907SAtari911 if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) { 21219378907SAtari911 $html .= '<td class="cal-empty"></td>'; 21319378907SAtari911 } else { 21419378907SAtari911 $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay); 21519378907SAtari911 $isToday = ($dateKey === date('Y-m-d')); 216*87ac9bf3SAtari911 $hasEvents = isset($eventRanges[$dateKey]) && !empty($eventRanges[$dateKey]); 21719378907SAtari911 21819378907SAtari911 $classes = 'cal-day'; 21919378907SAtari911 if ($isToday) $classes .= ' cal-today'; 22019378907SAtari911 if ($hasEvents) $classes .= ' cal-has-events'; 22119378907SAtari911 22219378907SAtari911 $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">'; 22319378907SAtari911 $html .= '<span class="day-num">' . $currentDay . '</span>'; 22419378907SAtari911 22519378907SAtari911 if ($hasEvents) { 22619378907SAtari911 // Sort events by time (no time first, then by time) 227*87ac9bf3SAtari911 $sortedEvents = $eventRanges[$dateKey]; 22819378907SAtari911 usort($sortedEvents, function($a, $b) { 22919378907SAtari911 $timeA = isset($a['time']) ? $a['time'] : ''; 23019378907SAtari911 $timeB = isset($b['time']) ? $b['time'] : ''; 23119378907SAtari911 23219378907SAtari911 // Events without time go first 23319378907SAtari911 if (empty($timeA) && !empty($timeB)) return -1; 23419378907SAtari911 if (!empty($timeA) && empty($timeB)) return 1; 23519378907SAtari911 if (empty($timeA) && empty($timeB)) return 0; 23619378907SAtari911 23719378907SAtari911 // Sort by time 23819378907SAtari911 return strcmp($timeA, $timeB); 23919378907SAtari911 }); 24019378907SAtari911 24119378907SAtari911 // Show colored stacked bars for each event 24219378907SAtari911 $html .= '<div class="event-indicators">'; 24319378907SAtari911 foreach ($sortedEvents as $evt) { 24419378907SAtari911 $eventId = isset($evt['id']) ? $evt['id'] : ''; 24519378907SAtari911 $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db'; 24619378907SAtari911 $eventTime = isset($evt['time']) ? $evt['time'] : ''; 24719378907SAtari911 $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event'; 248*87ac9bf3SAtari911 $originalDate = isset($evt['_original_date']) ? $evt['_original_date'] : $dateKey; 249*87ac9bf3SAtari911 $isFirstDay = isset($evt['_is_first_day']) ? $evt['_is_first_day'] : true; 250*87ac9bf3SAtari911 $isLastDay = isset($evt['_is_last_day']) ? $evt['_is_last_day'] : true; 25119378907SAtari911 25219378907SAtari911 $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed'; 25319378907SAtari911 254*87ac9bf3SAtari911 // Add classes for multi-day spanning 255*87ac9bf3SAtari911 if (!$isFirstDay) $barClass .= ' event-bar-continues'; 256*87ac9bf3SAtari911 if (!$isLastDay) $barClass .= ' event-bar-continuing'; 257*87ac9bf3SAtari911 25819378907SAtari911 $html .= '<span class="event-bar ' . $barClass . '" '; 25919378907SAtari911 $html .= 'style="background: ' . $eventColor . ';" '; 26019378907SAtari911 $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" '; 261*87ac9bf3SAtari911 $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $originalDate . '\');">'; 26219378907SAtari911 $html .= '</span>'; 26319378907SAtari911 } 26419378907SAtari911 $html .= '</div>'; 26519378907SAtari911 } 26619378907SAtari911 26719378907SAtari911 $html .= '</td>'; 26819378907SAtari911 $currentDay++; 26919378907SAtari911 } 27019378907SAtari911 } 27119378907SAtari911 $html .= '</tr>'; 27219378907SAtari911 } 27319378907SAtari911 27419378907SAtari911 $html .= '</tbody></table>'; 27519378907SAtari911 $html .= '</div>'; // End calendar-left 27619378907SAtari911 27719378907SAtari911 // Right side: Event list 27819378907SAtari911 $html .= '<div class="calendar-compact-right">'; 27919378907SAtari911 $html .= '<div class="event-list-header">'; 28019378907SAtari911 $html .= '<div class="event-list-header-content">'; 28119378907SAtari911 $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>'; 28219378907SAtari911 if ($namespace) { 28319378907SAtari911 $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>'; 28419378907SAtari911 } 28519378907SAtari911 $html .= '</div>'; 28619378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>'; 28719378907SAtari911 $html .= '</div>'; 28819378907SAtari911 28919378907SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">'; 29019378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 29119378907SAtari911 $html .= '</div>'; 29219378907SAtari911 29319378907SAtari911 $html .= '</div>'; // End calendar-right 29419378907SAtari911 29519378907SAtari911 // Event dialog 29619378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 29719378907SAtari911 298*87ac9bf3SAtari911 // Month/Year picker dialog (at container level for proper overlay) 299*87ac9bf3SAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace); 300*87ac9bf3SAtari911 30119378907SAtari911 $html .= '</div>'; // End container 30219378907SAtari911 30319378907SAtari911 return $html; 30419378907SAtari911 } 30519378907SAtari911 30619378907SAtari911 private function renderEventListContent($events, $calId, $namespace) { 30719378907SAtari911 if (empty($events)) { 30819378907SAtari911 return '<p class="no-events-msg">No events this month</p>'; 30919378907SAtari911 } 31019378907SAtari911 31119378907SAtari911 $html = ''; 31219378907SAtari911 ksort($events); 31319378907SAtari911 31419378907SAtari911 foreach ($events as $dateKey => $dayEvents) { 31519378907SAtari911 foreach ($dayEvents as $event) { 31619378907SAtari911 $eventId = isset($event['id']) ? $event['id'] : ''; 31719378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 31819378907SAtari911 $time = isset($event['time']) ? htmlspecialchars($event['time']) : ''; 31919378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 32019378907SAtari911 $description = isset($event['description']) ? $event['description'] : ''; 32119378907SAtari911 $isTask = isset($event['isTask']) ? $event['isTask'] : false; 32219378907SAtari911 $completed = isset($event['completed']) ? $event['completed'] : false; 32319378907SAtari911 $endDate = isset($event['endDate']) ? $event['endDate'] : ''; 32419378907SAtari911 32519378907SAtari911 // Process description for wiki syntax, HTML, images, and links 32619378907SAtari911 $renderedDescription = $this->renderDescription($description); 32719378907SAtari911 32819378907SAtari911 // Convert to 12-hour format 32919378907SAtari911 $displayTime = ''; 33019378907SAtari911 if ($time) { 33119378907SAtari911 $timeObj = DateTime::createFromFormat('H:i', $time); 33219378907SAtari911 if ($timeObj) { 33319378907SAtari911 $displayTime = $timeObj->format('g:i A'); 33419378907SAtari911 } else { 33519378907SAtari911 $displayTime = $time; 33619378907SAtari911 } 33719378907SAtari911 } 33819378907SAtari911 339*87ac9bf3SAtari911 // Format date display with day of week 34019378907SAtari911 $dateObj = new DateTime($dateKey); 341*87ac9bf3SAtari911 $displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24" 34219378907SAtari911 34319378907SAtari911 // Multi-day indicator 34419378907SAtari911 $multiDay = ''; 34519378907SAtari911 if ($endDate && $endDate !== $dateKey) { 34619378907SAtari911 $endObj = new DateTime($endDate); 347*87ac9bf3SAtari911 $multiDay = ' → ' . $endObj->format('D, M j'); 34819378907SAtari911 } 34919378907SAtari911 35019378907SAtari911 $completedClass = $completed ? ' event-completed' : ''; 35119378907SAtari911 35219378907SAtari911 $html .= '<div class="event-compact-item' . $completedClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';">'; 35319378907SAtari911 35419378907SAtari911 $html .= '<div class="event-info">'; 35519378907SAtari911 $html .= '<div class="event-title-row">'; 35619378907SAtari911 $html .= '<span class="event-title-compact">' . $title . '</span>'; 35719378907SAtari911 $html .= '</div>'; 35819378907SAtari911 35919378907SAtari911 $html .= '<div class="event-meta-compact">'; 36019378907SAtari911 $html .= '<span class="event-date-time">' . $displayDate . $multiDay; 36119378907SAtari911 if ($displayTime) { 36219378907SAtari911 $html .= ' • ' . $displayTime; 36319378907SAtari911 } 36419378907SAtari911 $html .= '</span>'; 36519378907SAtari911 $html .= '</div>'; 36619378907SAtari911 36719378907SAtari911 if ($description) { 36819378907SAtari911 $html .= '<div class="event-desc-compact">' . $renderedDescription . '</div>'; 36919378907SAtari911 } 37019378907SAtari911 37119378907SAtari911 $html .= '</div>'; // event-info 37219378907SAtari911 37319378907SAtari911 $html .= '<div class="event-actions-compact">'; 37419378907SAtari911 $html .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">️</button>'; 37519378907SAtari911 $html .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">✏️</button>'; 37619378907SAtari911 $html .= '</div>'; 37719378907SAtari911 37819378907SAtari911 // Checkbox for tasks - ON THE FAR RIGHT 37919378907SAtari911 if ($isTask) { 38019378907SAtari911 $checked = $completed ? 'checked' : ''; 38119378907SAtari911 $html .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\', this.checked)">'; 38219378907SAtari911 } 38319378907SAtari911 38419378907SAtari911 $html .= '</div>'; 38519378907SAtari911 } 38619378907SAtari911 } 38719378907SAtari911 38819378907SAtari911 return $html; 38919378907SAtari911 } 39019378907SAtari911 39119378907SAtari911 private function renderEventPanelOnly($data) { 39219378907SAtari911 $year = (int)$data['year']; 39319378907SAtari911 $month = (int)$data['month']; 39419378907SAtari911 $namespace = $data['namespace']; 395*87ac9bf3SAtari911 $height = isset($data['height']) ? $data['height'] : '400px'; 396*87ac9bf3SAtari911 397*87ac9bf3SAtari911 // Validate height format (must be px, em, rem, vh, or %) 398*87ac9bf3SAtari911 if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) { 399*87ac9bf3SAtari911 $height = '400px'; // Default fallback 400*87ac9bf3SAtari911 } 40119378907SAtari911 40219378907SAtari911 $events = $this->loadEvents($namespace, $year, $month); 40319378907SAtari911 $calId = 'panel_' . md5(serialize($data) . microtime()); 40419378907SAtari911 40519378907SAtari911 $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year)); 40619378907SAtari911 40719378907SAtari911 $prevMonth = $month - 1; 40819378907SAtari911 $prevYear = $year; 40919378907SAtari911 if ($prevMonth < 1) { 41019378907SAtari911 $prevMonth = 12; 41119378907SAtari911 $prevYear--; 41219378907SAtari911 } 41319378907SAtari911 41419378907SAtari911 $nextMonth = $month + 1; 41519378907SAtari911 $nextYear = $year; 41619378907SAtari911 if ($nextMonth > 12) { 41719378907SAtari911 $nextMonth = 1; 41819378907SAtari911 $nextYear++; 41919378907SAtari911 } 42019378907SAtari911 421*87ac9bf3SAtari911 $html = '<div class="event-panel-standalone" id="' . $calId . '" data-height="' . htmlspecialchars($height) . '" data-namespace="' . htmlspecialchars($namespace) . '">'; 42219378907SAtari911 42319378907SAtari911 // Header with navigation 42419378907SAtari911 $html .= '<div class="panel-standalone-header">'; 42519378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>'; 426*87ac9bf3SAtari911 $html .= '<div class="panel-header-content">'; 427*87ac9bf3SAtari911 $html .= '<h3 class="calendar-month-picker" onclick="openMonthPickerPanel(\'' . $calId . '\', ' . $year . ', ' . $month . ', \'' . $namespace . '\')" title="Click to jump to month">' . $monthName . ' Events</h3>'; 428*87ac9bf3SAtari911 if ($namespace) { 429*87ac9bf3SAtari911 $namespaceUrl = DOKU_BASE . 'doku.php?id=' . str_replace(':', ':', $namespace); 430*87ac9bf3SAtari911 $html .= '<a href="' . $namespaceUrl . '" class="namespace-badge" title="Go to namespace page">' . htmlspecialchars($namespace) . '</a>'; 431*87ac9bf3SAtari911 } 432*87ac9bf3SAtari911 $html .= '</div>'; 43319378907SAtari911 $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>'; 434*87ac9bf3SAtari911 $html .= '<button class="cal-today-btn" onclick="jumpTodayPanel(\'' . $calId . '\', \'' . $namespace . '\')">Today</button>'; 43519378907SAtari911 $html .= '</div>'; 43619378907SAtari911 43719378907SAtari911 $html .= '<div class="panel-standalone-actions">'; 43819378907SAtari911 $html .= '<button class="add-event-compact" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add Event</button>'; 43919378907SAtari911 $html .= '</div>'; 44019378907SAtari911 441*87ac9bf3SAtari911 $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '" style="max-height: ' . htmlspecialchars($height) . ';">'; 44219378907SAtari911 $html .= $this->renderEventListContent($events, $calId, $namespace); 44319378907SAtari911 $html .= '</div>'; 44419378907SAtari911 44519378907SAtari911 $html .= $this->renderEventDialog($calId, $namespace); 44619378907SAtari911 447*87ac9bf3SAtari911 // Month/Year picker for event panel 448*87ac9bf3SAtari911 $html .= $this->renderMonthPicker($calId, $year, $month, $namespace); 449*87ac9bf3SAtari911 45019378907SAtari911 $html .= '</div>'; 45119378907SAtari911 45219378907SAtari911 return $html; 45319378907SAtari911 } 45419378907SAtari911 45519378907SAtari911 private function renderStandaloneEventList($data) { 45619378907SAtari911 $namespace = $data['namespace']; 45719378907SAtari911 $daterange = $data['daterange']; 45819378907SAtari911 $date = $data['date']; 459*87ac9bf3SAtari911 $width = isset($data['width']) ? $data['width'] : '300px'; 460*87ac9bf3SAtari911 $height = isset($data['height']) ? $data['height'] : '400px'; 461*87ac9bf3SAtari911 $today = isset($data['today']) ? true : false; 46219378907SAtari911 463*87ac9bf3SAtari911 // Validate width/height format 464*87ac9bf3SAtari911 if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|vw|%)$/', $width)) { 465*87ac9bf3SAtari911 $width = '300px'; 466*87ac9bf3SAtari911 } 467*87ac9bf3SAtari911 if (!preg_match('/^\d+(\.\d+)?(px|em|rem|vh|%)$/', $height)) { 468*87ac9bf3SAtari911 $height = '400px'; 469*87ac9bf3SAtari911 } 470*87ac9bf3SAtari911 471*87ac9bf3SAtari911 // Handle "today" parameter 472*87ac9bf3SAtari911 if ($today) { 473*87ac9bf3SAtari911 $startDate = date('Y-m-d'); 474*87ac9bf3SAtari911 $endDate = date('Y-m-d'); 475*87ac9bf3SAtari911 } elseif ($daterange) { 47619378907SAtari911 list($startDate, $endDate) = explode(':', $daterange); 47719378907SAtari911 } elseif ($date) { 47819378907SAtari911 $startDate = $date; 47919378907SAtari911 $endDate = $date; 48019378907SAtari911 } else { 48119378907SAtari911 $startDate = date('Y-m-01'); 48219378907SAtari911 $endDate = date('Y-m-t'); 48319378907SAtari911 } 48419378907SAtari911 48519378907SAtari911 $allEvents = array(); 48619378907SAtari911 $start = new DateTime($startDate); 48719378907SAtari911 $end = new DateTime($endDate); 48819378907SAtari911 $end->modify('+1 day'); 48919378907SAtari911 49019378907SAtari911 $interval = new DateInterval('P1D'); 49119378907SAtari911 $period = new DatePeriod($start, $interval, $end); 49219378907SAtari911 49319378907SAtari911 static $loadedMonths = array(); 49419378907SAtari911 49519378907SAtari911 foreach ($period as $dt) { 49619378907SAtari911 $year = (int)$dt->format('Y'); 49719378907SAtari911 $month = (int)$dt->format('n'); 49819378907SAtari911 $dateKey = $dt->format('Y-m-d'); 49919378907SAtari911 50019378907SAtari911 $monthKey = $year . '-' . $month; 50119378907SAtari911 50219378907SAtari911 if (!isset($loadedMonths[$monthKey])) { 50319378907SAtari911 $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month); 50419378907SAtari911 } 50519378907SAtari911 50619378907SAtari911 $monthEvents = $loadedMonths[$monthKey]; 50719378907SAtari911 50819378907SAtari911 if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) { 50919378907SAtari911 $allEvents[$dateKey] = $monthEvents[$dateKey]; 51019378907SAtari911 } 51119378907SAtari911 } 51219378907SAtari911 513*87ac9bf3SAtari911 // Compact container with custom size 514*87ac9bf3SAtari911 $html = '<div class="eventlist-compact-widget" style="width: ' . htmlspecialchars($width) . '; max-height: ' . htmlspecialchars($height) . ';">'; 515*87ac9bf3SAtari911 516*87ac9bf3SAtari911 // Compact header 517*87ac9bf3SAtari911 if ($today) { 518*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-header">'; 519*87ac9bf3SAtari911 $html .= '<h4> Today\'s Events</h4>'; 520*87ac9bf3SAtari911 $html .= '</div>'; 521*87ac9bf3SAtari911 } else { 522*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-header">'; 523*87ac9bf3SAtari911 $html .= '<h4>' . date('M j', strtotime($startDate)); 524*87ac9bf3SAtari911 if ($startDate !== $endDate) { 525*87ac9bf3SAtari911 $html .= ' - ' . date('M j', strtotime($endDate)); 526*87ac9bf3SAtari911 } 527*87ac9bf3SAtari911 $html .= '</h4>'; 528*87ac9bf3SAtari911 $html .= '</div>'; 529*87ac9bf3SAtari911 } 530*87ac9bf3SAtari911 531*87ac9bf3SAtari911 // Scrollable event list 532*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-content">'; 53319378907SAtari911 53419378907SAtari911 if (empty($allEvents)) { 535*87ac9bf3SAtari911 $html .= '<p class="eventlist-widget-empty">No events</p>'; 53619378907SAtari911 } else { 53719378907SAtari911 foreach ($allEvents as $dateKey => $dayEvents) { 538*87ac9bf3SAtari911 // Compact date header (only if not "today" mode or multi-day range) 539*87ac9bf3SAtari911 if (!$today && $startDate !== $endDate) { 540*87ac9bf3SAtari911 $dateObj = new DateTime($dateKey); 541*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-date">' . $dateObj->format('D, M j') . '</div>'; 542*87ac9bf3SAtari911 } 54319378907SAtari911 54419378907SAtari911 foreach ($dayEvents as $event) { 54519378907SAtari911 $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled'; 546*87ac9bf3SAtari911 $time = isset($event['time']) ? $event['time'] : ''; 54719378907SAtari911 $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db'; 548*87ac9bf3SAtari911 $description = isset($event['description']) ? $event['description'] : ''; 54919378907SAtari911 550*87ac9bf3SAtari911 // Convert time to 12-hour format 551*87ac9bf3SAtari911 $displayTime = ''; 55219378907SAtari911 if ($time) { 553*87ac9bf3SAtari911 $timeParts = explode(':', $time); 554*87ac9bf3SAtari911 if (count($timeParts) === 2) { 555*87ac9bf3SAtari911 $hour = (int)$timeParts[0]; 556*87ac9bf3SAtari911 $minute = $timeParts[1]; 557*87ac9bf3SAtari911 $ampm = $hour >= 12 ? 'PM' : 'AM'; 558*87ac9bf3SAtari911 $hour = $hour % 12; 559*87ac9bf3SAtari911 if ($hour === 0) $hour = 12; 560*87ac9bf3SAtari911 $displayTime = $hour . ':' . $minute . ' ' . $ampm; 561*87ac9bf3SAtari911 } else { 562*87ac9bf3SAtari911 $displayTime = $time; 56319378907SAtari911 } 564*87ac9bf3SAtari911 } 565*87ac9bf3SAtari911 566*87ac9bf3SAtari911 // Compact event item 567*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-item" style="border-left-color: ' . $color . ';">'; 568*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-title">' . $title . '</div>'; 569*87ac9bf3SAtari911 if ($displayTime) { 570*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-time">' . $displayTime . '</div>'; 571*87ac9bf3SAtari911 } 57219378907SAtari911 if ($description) { 573*87ac9bf3SAtari911 $renderedDesc = $this->renderDescription($description); 574*87ac9bf3SAtari911 $html .= '<div class="eventlist-widget-desc">' . $renderedDesc . '</div>'; 57519378907SAtari911 } 57619378907SAtari911 $html .= '</div>'; 57719378907SAtari911 } 57819378907SAtari911 } 579*87ac9bf3SAtari911 } 58019378907SAtari911 581*87ac9bf3SAtari911 $html .= '</div>'; // End content 582*87ac9bf3SAtari911 $html .= '</div>'; // End container 58319378907SAtari911 58419378907SAtari911 return $html; 58519378907SAtari911 } 58619378907SAtari911 58719378907SAtari911 private function renderEventDialog($calId, $namespace) { 58819378907SAtari911 $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">'; 58919378907SAtari911 $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>'; 59019378907SAtari911 59119378907SAtari911 // Draggable dialog 59219378907SAtari911 $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">'; 59319378907SAtari911 59419378907SAtari911 // Header with drag handle and close button 59519378907SAtari911 $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">'; 59619378907SAtari911 $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>'; 59719378907SAtari911 $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>'; 59819378907SAtari911 $html .= '</div>'; 59919378907SAtari911 60019378907SAtari911 // Form content 60119378907SAtari911 $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">'; 60219378907SAtari911 60319378907SAtari911 // Hidden ID field 60419378907SAtari911 $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">'; 60519378907SAtari911 60619378907SAtari911 // Task checkbox 60719378907SAtari911 $html .= '<div class="form-field form-field-checkbox">'; 60819378907SAtari911 $html .= '<label class="checkbox-label">'; 60919378907SAtari911 $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">'; 61019378907SAtari911 $html .= '<span> This is a task (can be checked off)</span>'; 61119378907SAtari911 $html .= '</label>'; 61219378907SAtari911 $html .= '</div>'; 61319378907SAtari911 61419378907SAtari911 // Date and Time in a row 61519378907SAtari911 $html .= '<div class="form-row-group">'; 61619378907SAtari911 61719378907SAtari911 // Start Date field 61819378907SAtari911 $html .= '<div class="form-field form-field-date">'; 61919378907SAtari911 $html .= '<label class="field-label"> Start Date</label>'; 62019378907SAtari911 $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date">'; 62119378907SAtari911 $html .= '</div>'; 62219378907SAtari911 62319378907SAtari911 // End Date field (for multi-day events) 62419378907SAtari911 $html .= '<div class="form-field form-field-date">'; 62519378907SAtari911 $html .= '<label class="field-label"> End Date</label>'; 62619378907SAtari911 $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date">'; 62719378907SAtari911 $html .= '</div>'; 62819378907SAtari911 62919378907SAtari911 $html .= '</div>'; 63019378907SAtari911 631*87ac9bf3SAtari911 // Recurring event section 632*87ac9bf3SAtari911 $html .= '<div class="form-field form-field-checkbox">'; 633*87ac9bf3SAtari911 $html .= '<label class="checkbox-label">'; 634*87ac9bf3SAtari911 $html .= '<input type="checkbox" id="event-recurring-' . $calId . '" name="isRecurring" class="recurring-toggle" onchange="toggleRecurringOptions(\'' . $calId . '\')">'; 635*87ac9bf3SAtari911 $html .= '<span> Repeating Event</span>'; 636*87ac9bf3SAtari911 $html .= '</label>'; 637*87ac9bf3SAtari911 $html .= '</div>'; 638*87ac9bf3SAtari911 639*87ac9bf3SAtari911 // Recurring options (hidden by default) 640*87ac9bf3SAtari911 $html .= '<div id="recurring-options-' . $calId . '" class="recurring-options" style="display:none;">'; 641*87ac9bf3SAtari911 642*87ac9bf3SAtari911 // Recurrence pattern 643*87ac9bf3SAtari911 $html .= '<div class="form-field">'; 644*87ac9bf3SAtari911 $html .= '<label class="field-label">Repeat Every</label>'; 645*87ac9bf3SAtari911 $html .= '<select id="event-recurrence-type-' . $calId . '" name="recurrenceType" class="input-sleek">'; 646*87ac9bf3SAtari911 $html .= '<option value="daily">Daily</option>'; 647*87ac9bf3SAtari911 $html .= '<option value="weekly">Weekly</option>'; 648*87ac9bf3SAtari911 $html .= '<option value="monthly">Monthly</option>'; 649*87ac9bf3SAtari911 $html .= '<option value="yearly">Yearly</option>'; 650*87ac9bf3SAtari911 $html .= '</select>'; 651*87ac9bf3SAtari911 $html .= '</div>'; 652*87ac9bf3SAtari911 653*87ac9bf3SAtari911 // Recurrence end date 654*87ac9bf3SAtari911 $html .= '<div class="form-field">'; 655*87ac9bf3SAtari911 $html .= '<label class="field-label"> Repeat Until (optional)</label>'; 656*87ac9bf3SAtari911 $html .= '<input type="date" id="event-recurrence-end-' . $calId . '" name="recurrenceEnd" class="input-sleek input-date">'; 657*87ac9bf3SAtari911 $html .= '</div>'; 658*87ac9bf3SAtari911 659*87ac9bf3SAtari911 $html .= '</div>'; 660*87ac9bf3SAtari911 66119378907SAtari911 // Time field 66219378907SAtari911 $html .= '<div class="form-field">'; 66319378907SAtari911 $html .= '<label class="field-label"> Time (optional)</label>'; 66419378907SAtari911 $html .= '<input type="time" id="event-time-' . $calId . '" name="time" class="input-sleek">'; 66519378907SAtari911 $html .= '</div>'; 66619378907SAtari911 66719378907SAtari911 // Title field 66819378907SAtari911 $html .= '<div class="form-field">'; 66919378907SAtari911 $html .= '<label class="field-label"> Title</label>'; 67019378907SAtari911 $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek" placeholder="Event or task title...">'; 67119378907SAtari911 $html .= '</div>'; 67219378907SAtari911 67319378907SAtari911 // Description field 67419378907SAtari911 $html .= '<div class="form-field">'; 67519378907SAtari911 $html .= '<label class="field-label"> Description</label>'; 67619378907SAtari911 $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="3" class="input-sleek textarea-sleek" placeholder="Add details (optional)..."></textarea>'; 67719378907SAtari911 $html .= '</div>'; 67819378907SAtari911 67919378907SAtari911 // Color picker 68019378907SAtari911 $html .= '<div class="form-field">'; 68119378907SAtari911 $html .= '<label class="field-label"> Color</label>'; 68219378907SAtari911 $html .= '<div class="color-picker-container">'; 68319378907SAtari911 $html .= '<input type="color" id="event-color-' . $calId . '" name="color" value="#3498db" class="input-color-sleek">'; 68419378907SAtari911 $html .= '<span class="color-label">Choose event color</span>'; 68519378907SAtari911 $html .= '</div>'; 68619378907SAtari911 $html .= '</div>'; 68719378907SAtari911 68819378907SAtari911 // Action buttons 68919378907SAtari911 $html .= '<div class="dialog-actions-sleek">'; 69019378907SAtari911 $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>'; 69119378907SAtari911 $html .= '<button type="submit" class="btn-sleek btn-save-sleek"> Save</button>'; 69219378907SAtari911 $html .= '</div>'; 69319378907SAtari911 69419378907SAtari911 $html .= '</form>'; 69519378907SAtari911 $html .= '</div>'; 69619378907SAtari911 $html .= '</div>'; 69719378907SAtari911 69819378907SAtari911 return $html; 69919378907SAtari911 } 70019378907SAtari911 701*87ac9bf3SAtari911 private function renderMonthPicker($calId, $year, $month, $namespace) { 702*87ac9bf3SAtari911 $html = '<div class="month-picker-overlay" id="month-picker-overlay-' . $calId . '" style="display:none;" onclick="closeMonthPicker(\'' . $calId . '\')">'; 703*87ac9bf3SAtari911 $html .= '<div class="month-picker-dialog" onclick="event.stopPropagation();">'; 704*87ac9bf3SAtari911 $html .= '<h4>Jump to Month</h4>'; 705*87ac9bf3SAtari911 706*87ac9bf3SAtari911 $html .= '<div class="month-picker-selects">'; 707*87ac9bf3SAtari911 $html .= '<select id="month-picker-month-' . $calId . '" class="month-picker-select">'; 708*87ac9bf3SAtari911 $monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; 709*87ac9bf3SAtari911 for ($m = 1; $m <= 12; $m++) { 710*87ac9bf3SAtari911 $selected = ($m == $month) ? ' selected' : ''; 711*87ac9bf3SAtari911 $html .= '<option value="' . $m . '"' . $selected . '>' . $monthNames[$m - 1] . '</option>'; 712*87ac9bf3SAtari911 } 713*87ac9bf3SAtari911 $html .= '</select>'; 714*87ac9bf3SAtari911 715*87ac9bf3SAtari911 $html .= '<select id="month-picker-year-' . $calId . '" class="month-picker-select">'; 716*87ac9bf3SAtari911 $currentYear = (int)date('Y'); 717*87ac9bf3SAtari911 for ($y = $currentYear - 5; $y <= $currentYear + 5; $y++) { 718*87ac9bf3SAtari911 $selected = ($y == $year) ? ' selected' : ''; 719*87ac9bf3SAtari911 $html .= '<option value="' . $y . '"' . $selected . '>' . $y . '</option>'; 720*87ac9bf3SAtari911 } 721*87ac9bf3SAtari911 $html .= '</select>'; 722*87ac9bf3SAtari911 $html .= '</div>'; 723*87ac9bf3SAtari911 724*87ac9bf3SAtari911 $html .= '<div class="month-picker-actions">'; 725*87ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-cancel-sleek" onclick="closeMonthPicker(\'' . $calId . '\')">Cancel</button>'; 726*87ac9bf3SAtari911 $html .= '<button class="btn-sleek btn-save-sleek" onclick="jumpToSelectedMonth(\'' . $calId . '\', \'' . $namespace . '\')">Go</button>'; 727*87ac9bf3SAtari911 $html .= '</div>'; 728*87ac9bf3SAtari911 729*87ac9bf3SAtari911 $html .= '</div>'; 730*87ac9bf3SAtari911 $html .= '</div>'; 731*87ac9bf3SAtari911 732*87ac9bf3SAtari911 return $html; 733*87ac9bf3SAtari911 } 734*87ac9bf3SAtari911 73519378907SAtari911 private function renderDescription($description) { 73619378907SAtari911 if (empty($description)) { 73719378907SAtari911 return ''; 73819378907SAtari911 } 73919378907SAtari911 74019378907SAtari911 // Convert newlines to <br> for basic formatting 74119378907SAtari911 $rendered = nl2br($description); 74219378907SAtari911 74319378907SAtari911 // Convert DokuWiki image syntax {{image.jpg}} to HTML 74419378907SAtari911 $rendered = preg_replace_callback( 74519378907SAtari911 '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/', 74619378907SAtari911 function($matches) { 74719378907SAtari911 $imagePath = trim($matches[1]); 74819378907SAtari911 $alt = isset($matches[2]) ? trim($matches[2]) : ''; 74919378907SAtari911 75019378907SAtari911 // Handle external URLs (http:// or https://) 75119378907SAtari911 if (preg_match('/^https?:\/\//', $imagePath)) { 75219378907SAtari911 return '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 75319378907SAtari911 } 75419378907SAtari911 75519378907SAtari911 // Handle internal DokuWiki images 75619378907SAtari911 $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath); 75719378907SAtari911 return '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />'; 75819378907SAtari911 }, 75919378907SAtari911 $rendered 76019378907SAtari911 ); 76119378907SAtari911 76219378907SAtari911 // Convert DokuWiki link syntax [[link|text]] to HTML 76319378907SAtari911 $rendered = preg_replace_callback( 76419378907SAtari911 '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/', 76519378907SAtari911 function($matches) { 76619378907SAtari911 $link = trim($matches[1]); 76719378907SAtari911 $text = isset($matches[2]) ? trim($matches[2]) : $link; 76819378907SAtari911 76919378907SAtari911 // Handle external URLs 77019378907SAtari911 if (preg_match('/^https?:\/\//', $link)) { 77119378907SAtari911 return '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 77219378907SAtari911 } 77319378907SAtari911 774*87ac9bf3SAtari911 // Handle internal DokuWiki links with section anchors 775*87ac9bf3SAtari911 // Split page and section (e.g., "page#section" or "namespace:page#section") 776*87ac9bf3SAtari911 $parts = explode('#', $link, 2); 777*87ac9bf3SAtari911 $pagePart = $parts[0]; 778*87ac9bf3SAtari911 $sectionPart = isset($parts[1]) ? '#' . $parts[1] : ''; 779*87ac9bf3SAtari911 780*87ac9bf3SAtari911 // Build URL with properly encoded page and unencoded section anchor 781*87ac9bf3SAtari911 $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart; 78219378907SAtari911 return '<a href="' . $wikiUrl . '">' . htmlspecialchars($text) . '</a>'; 78319378907SAtari911 }, 78419378907SAtari911 $rendered 78519378907SAtari911 ); 78619378907SAtari911 78719378907SAtari911 // Convert markdown-style links [text](url) to HTML 78819378907SAtari911 $rendered = preg_replace_callback( 78919378907SAtari911 '/\[([^\]]+)\]\(([^)]+)\)/', 79019378907SAtari911 function($matches) { 79119378907SAtari911 $text = trim($matches[1]); 79219378907SAtari911 $url = trim($matches[2]); 79319378907SAtari911 79419378907SAtari911 if (preg_match('/^https?:\/\//', $url)) { 79519378907SAtari911 return '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>'; 79619378907SAtari911 } 79719378907SAtari911 79819378907SAtari911 return '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars($text) . '</a>'; 79919378907SAtari911 }, 80019378907SAtari911 $rendered 80119378907SAtari911 ); 80219378907SAtari911 80319378907SAtari911 // Convert plain URLs to clickable links 80419378907SAtari911 $rendered = preg_replace_callback( 80519378907SAtari911 '/(https?:\/\/[^\s<]+)/', 80619378907SAtari911 function($matches) { 80719378907SAtari911 $url = $matches[1]; 80819378907SAtari911 return '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>'; 80919378907SAtari911 }, 81019378907SAtari911 $rendered 81119378907SAtari911 ); 81219378907SAtari911 81319378907SAtari911 // Allow basic HTML tags (bold, italic, strong, em, u, code) 81419378907SAtari911 // Already in the description, just pass through 81519378907SAtari911 81619378907SAtari911 return $rendered; 81719378907SAtari911 } 81819378907SAtari911 81919378907SAtari911 private function loadEvents($namespace, $year, $month) { 82019378907SAtari911 $dataDir = DOKU_INC . 'data/meta/'; 82119378907SAtari911 if ($namespace) { 82219378907SAtari911 $dataDir .= str_replace(':', '/', $namespace) . '/'; 82319378907SAtari911 } 82419378907SAtari911 $dataDir .= 'calendar/'; 82519378907SAtari911 82619378907SAtari911 $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month); 82719378907SAtari911 82819378907SAtari911 if (file_exists($eventFile)) { 82919378907SAtari911 $json = file_get_contents($eventFile); 83019378907SAtari911 return json_decode($json, true); 83119378907SAtari911 } 83219378907SAtari911 83319378907SAtari911 return array(); 83419378907SAtari911 } 83519378907SAtari911} 836