xref: /plugin/calendar/syntax.php (revision 87ac9bf3391b3f7059f4ccd6abc619e9db5fad8d)
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