xref: /plugin/calendar/syntax.php (revision 19378907f6c3c154fcddd2ddfe78fa2d88d43359)
1<?php
2/**
3 * DokuWiki Plugin calendar (Syntax Component)
4 * Compact design with integrated event list
5 *
6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
7 * @author  DokuWiki Community
8 */
9
10if (!defined('DOKU_INC')) die();
11
12class syntax_plugin_calendar extends DokuWiki_Syntax_Plugin {
13
14    public function getType() {
15        return 'substition';
16    }
17
18    public function getPType() {
19        return 'block';
20    }
21
22    public function getSort() {
23        return 155;
24    }
25
26    public function connectTo($mode) {
27        $this->Lexer->addSpecialPattern('\{\{calendar(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
28        $this->Lexer->addSpecialPattern('\{\{eventlist(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
29        $this->Lexer->addSpecialPattern('\{\{eventpanel(?:[^\}]*)\}\}', $mode, 'plugin_calendar');
30    }
31
32    public function handle($match, $state, $pos, Doku_Handler $handler) {
33        $isEventList = (strpos($match, '{{eventlist') === 0);
34        $isEventPanel = (strpos($match, '{{eventpanel') === 0);
35
36        if ($isEventList) {
37            $match = substr($match, 12, -2);
38        } elseif ($isEventPanel) {
39            $match = substr($match, 13, -2);
40        } else {
41            $match = substr($match, 10, -2);
42        }
43
44        $params = array(
45            'type' => $isEventPanel ? 'eventpanel' : ($isEventList ? 'eventlist' : 'calendar'),
46            'year' => date('Y'),
47            'month' => date('n'),
48            'namespace' => '',
49            'daterange' => '',
50            'date' => ''
51        );
52
53        if (trim($match)) {
54            $pairs = preg_split('/\s+/', trim($match));
55            foreach ($pairs as $pair) {
56                if (strpos($pair, '=') !== false) {
57                    list($key, $value) = explode('=', $pair, 2);
58                    $params[trim($key)] = trim($value);
59                }
60            }
61        }
62
63        return $params;
64    }
65
66    public function render($mode, Doku_Renderer $renderer, $data) {
67        if ($mode !== 'xhtml') return false;
68
69        if ($data['type'] === 'eventlist') {
70            $html = $this->renderStandaloneEventList($data);
71        } elseif ($data['type'] === 'eventpanel') {
72            $html = $this->renderEventPanelOnly($data);
73        } else {
74            $html = $this->renderCompactCalendar($data);
75        }
76
77        $renderer->doc .= $html;
78        return true;
79    }
80
81    private function renderCompactCalendar($data) {
82        $year = (int)$data['year'];
83        $month = (int)$data['month'];
84        $namespace = $data['namespace'];
85
86        $events = $this->loadEvents($namespace, $year, $month);
87        $calId = 'cal_' . md5(serialize($data) . microtime());
88
89        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
90
91        $prevMonth = $month - 1;
92        $prevYear = $year;
93        if ($prevMonth < 1) {
94            $prevMonth = 12;
95            $prevYear--;
96        }
97
98        $nextMonth = $month + 1;
99        $nextYear = $year;
100        if ($nextMonth > 12) {
101            $nextMonth = 1;
102            $nextYear++;
103        }
104
105        $html = '<div class="calendar-compact-container" id="' . $calId . '" data-namespace="' . htmlspecialchars($namespace) . '" data-year="' . $year . '" data-month="' . $month . '">';
106
107        // Embed events data as JSON for JavaScript access
108        $html .= '<script type="application/json" id="events-data-' . $calId . '">' . json_encode($events) . '</script>';
109
110        // Left side: Calendar
111        $html .= '<div class="calendar-compact-left">';
112
113        // Header with navigation
114        $html .= '<div class="calendar-compact-header">';
115        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
116        $html .= '<h3>' . $monthName . '</h3>';
117        $html .= '<button class="cal-nav-btn" onclick="navCalendar(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
118        $html .= '</div>';
119
120        // Calendar grid
121        $html .= '<table class="calendar-compact-grid">';
122        $html .= '<thead><tr>';
123        $html .= '<th>S</th><th>M</th><th>T</th><th>W</th><th>T</th><th>F</th><th>S</th>';
124        $html .= '</tr></thead><tbody>';
125
126        $firstDay = mktime(0, 0, 0, $month, 1, $year);
127        $daysInMonth = date('t', $firstDay);
128        $dayOfWeek = date('w', $firstDay);
129
130        $currentDay = 1;
131        $rowCount = ceil(($daysInMonth + $dayOfWeek) / 7);
132
133        for ($row = 0; $row < $rowCount; $row++) {
134            $html .= '<tr>';
135            for ($col = 0; $col < 7; $col++) {
136                if (($row === 0 && $col < $dayOfWeek) || $currentDay > $daysInMonth) {
137                    $html .= '<td class="cal-empty"></td>';
138                } else {
139                    $dateKey = sprintf('%04d-%02d-%02d', $year, $month, $currentDay);
140                    $isToday = ($dateKey === date('Y-m-d'));
141                    $hasEvents = isset($events[$dateKey]) && !empty($events[$dateKey]);
142
143                    $classes = 'cal-day';
144                    if ($isToday) $classes .= ' cal-today';
145                    if ($hasEvents) $classes .= ' cal-has-events';
146
147                    $html .= '<td class="' . $classes . '" data-date="' . $dateKey . '" onclick="showDayPopup(\'' . $calId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">';
148                    $html .= '<span class="day-num">' . $currentDay . '</span>';
149
150                    if ($hasEvents) {
151                        // Sort events by time (no time first, then by time)
152                        $sortedEvents = $events[$dateKey];
153                        usort($sortedEvents, function($a, $b) {
154                            $timeA = isset($a['time']) ? $a['time'] : '';
155                            $timeB = isset($b['time']) ? $b['time'] : '';
156
157                            // Events without time go first
158                            if (empty($timeA) && !empty($timeB)) return -1;
159                            if (!empty($timeA) && empty($timeB)) return 1;
160                            if (empty($timeA) && empty($timeB)) return 0;
161
162                            // Sort by time
163                            return strcmp($timeA, $timeB);
164                        });
165
166                        // Show colored stacked bars for each event
167                        $html .= '<div class="event-indicators">';
168                        foreach ($sortedEvents as $evt) {
169                            $eventId = isset($evt['id']) ? $evt['id'] : '';
170                            $eventColor = isset($evt['color']) ? htmlspecialchars($evt['color']) : '#3498db';
171                            $eventTime = isset($evt['time']) ? $evt['time'] : '';
172                            $eventTitle = isset($evt['title']) ? htmlspecialchars($evt['title']) : 'Event';
173
174                            $barClass = empty($eventTime) ? 'event-bar-no-time' : 'event-bar-timed';
175
176                            $html .= '<span class="event-bar ' . $barClass . '" ';
177                            $html .= 'style="background: ' . $eventColor . ';" ';
178                            $html .= 'title="' . $eventTitle . ($eventTime ? ' @ ' . $eventTime : '') . '" ';
179                            $html .= 'onclick="event.stopPropagation(); highlightEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\');">';
180                            $html .= '</span>';
181                        }
182                        $html .= '</div>';
183                    }
184
185                    $html .= '</td>';
186                    $currentDay++;
187                }
188            }
189            $html .= '</tr>';
190        }
191
192        $html .= '</tbody></table>';
193        $html .= '</div>'; // End calendar-left
194
195        // Right side: Event list
196        $html .= '<div class="calendar-compact-right">';
197        $html .= '<div class="event-list-header">';
198        $html .= '<div class="event-list-header-content">';
199        $html .= '<h4 id="eventlist-title-' . $calId . '">Events</h4>';
200        if ($namespace) {
201            $html .= '<span class="namespace-badge">' . htmlspecialchars($namespace) . '</span>';
202        }
203        $html .= '</div>';
204        $html .= '<button class="add-event-compact" onclick="openAddEvent(\'' . $calId . '\', \'' . $namespace . '\')">+ Add</button>';
205        $html .= '</div>';
206
207        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
208        $html .= $this->renderEventListContent($events, $calId, $namespace);
209        $html .= '</div>';
210
211        $html .= '</div>'; // End calendar-right
212
213        // Event dialog
214        $html .= $this->renderEventDialog($calId, $namespace);
215
216        $html .= '</div>'; // End container
217
218        return $html;
219    }
220
221    private function renderEventListContent($events, $calId, $namespace) {
222        if (empty($events)) {
223            return '<p class="no-events-msg">No events this month</p>';
224        }
225
226        $html = '';
227        ksort($events);
228
229        foreach ($events as $dateKey => $dayEvents) {
230            foreach ($dayEvents as $event) {
231                $eventId = isset($event['id']) ? $event['id'] : '';
232                $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
233                $time = isset($event['time']) ? htmlspecialchars($event['time']) : '';
234                $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
235                $description = isset($event['description']) ? $event['description'] : '';
236                $isTask = isset($event['isTask']) ? $event['isTask'] : false;
237                $completed = isset($event['completed']) ? $event['completed'] : false;
238                $endDate = isset($event['endDate']) ? $event['endDate'] : '';
239
240                // Process description for wiki syntax, HTML, images, and links
241                $renderedDescription = $this->renderDescription($description);
242
243                // Convert to 12-hour format
244                $displayTime = '';
245                if ($time) {
246                    $timeObj = DateTime::createFromFormat('H:i', $time);
247                    if ($timeObj) {
248                        $displayTime = $timeObj->format('g:i A');
249                    } else {
250                        $displayTime = $time;
251                    }
252                }
253
254                // Format date display
255                $dateObj = new DateTime($dateKey);
256                $displayDate = $dateObj->format('M j');
257
258                // Multi-day indicator
259                $multiDay = '';
260                if ($endDate && $endDate !== $dateKey) {
261                    $endObj = new DateTime($endDate);
262                    $multiDay = ' → ' . $endObj->format('M j');
263                }
264
265                $completedClass = $completed ? ' event-completed' : '';
266
267                $html .= '<div class="event-compact-item' . $completedClass . '" data-event-id="' . $eventId . '" data-date="' . $dateKey . '" style="border-left-color: ' . $color . ';">';
268
269                $html .= '<div class="event-info">';
270                $html .= '<div class="event-title-row">';
271                $html .= '<span class="event-title-compact">' . $title . '</span>';
272                $html .= '</div>';
273
274                $html .= '<div class="event-meta-compact">';
275                $html .= '<span class="event-date-time">' . $displayDate . $multiDay;
276                if ($displayTime) {
277                    $html .= ' • ' . $displayTime;
278                }
279                $html .= '</span>';
280                $html .= '</div>';
281
282                if ($description) {
283                    $html .= '<div class="event-desc-compact">' . $renderedDescription . '</div>';
284                }
285
286                $html .= '</div>'; // event-info
287
288                $html .= '<div class="event-actions-compact">';
289                $html .= '<button class="event-action-btn" onclick="deleteEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">��️</button>';
290                $html .= '<button class="event-action-btn" onclick="editEvent(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\')">✏️</button>';
291                $html .= '</div>';
292
293                // Checkbox for tasks - ON THE FAR RIGHT
294                if ($isTask) {
295                    $checked = $completed ? 'checked' : '';
296                    $html .= '<input type="checkbox" class="task-checkbox" ' . $checked . ' onclick="toggleTaskComplete(\'' . $calId . '\', \'' . $eventId . '\', \'' . $dateKey . '\', \'' . $namespace . '\', this.checked)">';
297                }
298
299                $html .= '</div>';
300            }
301        }
302
303        return $html;
304    }
305
306    private function renderEventPanelOnly($data) {
307        $year = (int)$data['year'];
308        $month = (int)$data['month'];
309        $namespace = $data['namespace'];
310
311        $events = $this->loadEvents($namespace, $year, $month);
312        $calId = 'panel_' . md5(serialize($data) . microtime());
313
314        $monthName = date('F Y', mktime(0, 0, 0, $month, 1, $year));
315
316        $prevMonth = $month - 1;
317        $prevYear = $year;
318        if ($prevMonth < 1) {
319            $prevMonth = 12;
320            $prevYear--;
321        }
322
323        $nextMonth = $month + 1;
324        $nextYear = $year;
325        if ($nextMonth > 12) {
326            $nextMonth = 1;
327            $nextYear++;
328        }
329
330        $html = '<div class="event-panel-standalone" id="' . $calId . '">';
331
332        // Header with navigation
333        $html .= '<div class="panel-standalone-header">';
334        $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $prevYear . ', ' . $prevMonth . ', \'' . $namespace . '\')">‹</button>';
335        $html .= '<h3>' . $monthName . ' Events</h3>';
336        $html .= '<button class="cal-nav-btn" onclick="navEventPanel(\'' . $calId . '\', ' . $nextYear . ', ' . $nextMonth . ', \'' . $namespace . '\')">›</button>';
337        $html .= '</div>';
338
339        $html .= '<div class="panel-standalone-actions">';
340        $html .= '<button class="add-event-compact" onclick="openAddEventPanel(\'' . $calId . '\', \'' . $namespace . '\')">+ Add Event</button>';
341        $html .= '</div>';
342
343        $html .= '<div class="event-list-compact" id="eventlist-' . $calId . '">';
344        $html .= $this->renderEventListContent($events, $calId, $namespace);
345        $html .= '</div>';
346
347        $html .= $this->renderEventDialog($calId, $namespace);
348
349        $html .= '</div>';
350
351        return $html;
352    }
353
354    private function renderStandaloneEventList($data) {
355        $namespace = $data['namespace'];
356        $daterange = $data['daterange'];
357        $date = $data['date'];
358
359        if ($daterange) {
360            list($startDate, $endDate) = explode(':', $daterange);
361        } elseif ($date) {
362            $startDate = $date;
363            $endDate = $date;
364        } else {
365            $startDate = date('Y-m-01');
366            $endDate = date('Y-m-t');
367        }
368
369        $allEvents = array();
370        $start = new DateTime($startDate);
371        $end = new DateTime($endDate);
372        $end->modify('+1 day');
373
374        $interval = new DateInterval('P1D');
375        $period = new DatePeriod($start, $interval, $end);
376
377        static $loadedMonths = array();
378
379        foreach ($period as $dt) {
380            $year = (int)$dt->format('Y');
381            $month = (int)$dt->format('n');
382            $dateKey = $dt->format('Y-m-d');
383
384            $monthKey = $year . '-' . $month;
385
386            if (!isset($loadedMonths[$monthKey])) {
387                $loadedMonths[$monthKey] = $this->loadEvents($namespace, $year, $month);
388            }
389
390            $monthEvents = $loadedMonths[$monthKey];
391
392            if (isset($monthEvents[$dateKey]) && !empty($monthEvents[$dateKey])) {
393                $allEvents[$dateKey] = $monthEvents[$dateKey];
394            }
395        }
396
397        $html = '<div class="eventlist-standalone">';
398        $html .= '<h3>Events: ' . date('M j', strtotime($startDate)) . ' - ' . date('M j, Y', strtotime($endDate)) . '</h3>';
399
400        if (empty($allEvents)) {
401            $html .= '<p class="no-events-msg">No events in this date range</p>';
402        } else {
403            foreach ($allEvents as $dateKey => $dayEvents) {
404                $displayDate = date('l, F j, Y', strtotime($dateKey));
405
406                $html .= '<div class="eventlist-day-group">';
407                $html .= '<h4 class="eventlist-date">' . $displayDate . '</h4>';
408
409                foreach ($dayEvents as $event) {
410                    $title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
411                    $time = isset($event['time']) ? htmlspecialchars($event['time']) : '';
412                    $color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
413                    $description = isset($event['description']) ? htmlspecialchars($event['description']) : '';
414
415                    $html .= '<div class="eventlist-item">';
416                    $html .= '<div class="event-color-bar" style="background: ' . $color . ';"></div>';
417                    $html .= '<div class="eventlist-content">';
418                    if ($time) {
419                        $html .= '<span class="eventlist-time">' . $time . '</span>';
420                    }
421                    $html .= '<span class="eventlist-title">' . $title . '</span>';
422                    if ($description) {
423                        $html .= '<div class="eventlist-desc">' . nl2br($description) . '</div>';
424                    }
425                    $html .= '</div></div>';
426                }
427
428                $html .= '</div>';
429            }
430        }
431
432        $html .= '</div>';
433
434        return $html;
435    }
436
437    private function renderEventDialog($calId, $namespace) {
438        $html = '<div class="event-dialog-compact" id="dialog-' . $calId . '" style="display:none;">';
439        $html .= '<div class="dialog-overlay" onclick="closeEventDialog(\'' . $calId . '\')"></div>';
440
441        // Draggable dialog
442        $html .= '<div class="dialog-content-sleek" id="dialog-content-' . $calId . '">';
443
444        // Header with drag handle and close button
445        $html .= '<div class="dialog-header-sleek dialog-drag-handle" id="drag-handle-' . $calId . '">';
446        $html .= '<h3 id="dialog-title-' . $calId . '">Add Event</h3>';
447        $html .= '<button type="button" class="dialog-close-btn" onclick="closeEventDialog(\'' . $calId . '\')">×</button>';
448        $html .= '</div>';
449
450        // Form content
451        $html .= '<form id="eventform-' . $calId . '" onsubmit="saveEventCompact(\'' . $calId . '\', \'' . $namespace . '\'); return false;" class="sleek-form">';
452
453        // Hidden ID field
454        $html .= '<input type="hidden" id="event-id-' . $calId . '" name="eventId" value="">';
455
456        // Task checkbox
457        $html .= '<div class="form-field form-field-checkbox">';
458        $html .= '<label class="checkbox-label">';
459        $html .= '<input type="checkbox" id="event-is-task-' . $calId . '" name="isTask" class="task-toggle">';
460        $html .= '<span>�� This is a task (can be checked off)</span>';
461        $html .= '</label>';
462        $html .= '</div>';
463
464        // Date and Time in a row
465        $html .= '<div class="form-row-group">';
466
467        // Start Date field
468        $html .= '<div class="form-field form-field-date">';
469        $html .= '<label class="field-label">�� Start Date</label>';
470        $html .= '<input type="date" id="event-date-' . $calId . '" name="date" required class="input-sleek input-date">';
471        $html .= '</div>';
472
473        // End Date field (for multi-day events)
474        $html .= '<div class="form-field form-field-date">';
475        $html .= '<label class="field-label">�� End Date</label>';
476        $html .= '<input type="date" id="event-end-date-' . $calId . '" name="endDate" class="input-sleek input-date">';
477        $html .= '</div>';
478
479        $html .= '</div>';
480
481        // Time field
482        $html .= '<div class="form-field">';
483        $html .= '<label class="field-label">�� Time (optional)</label>';
484        $html .= '<input type="time" id="event-time-' . $calId . '" name="time" class="input-sleek">';
485        $html .= '</div>';
486
487        // Title field
488        $html .= '<div class="form-field">';
489        $html .= '<label class="field-label">�� Title</label>';
490        $html .= '<input type="text" id="event-title-' . $calId . '" name="title" required class="input-sleek" placeholder="Event or task title...">';
491        $html .= '</div>';
492
493        // Description field
494        $html .= '<div class="form-field">';
495        $html .= '<label class="field-label">�� Description</label>';
496        $html .= '<textarea id="event-desc-' . $calId . '" name="description" rows="3" class="input-sleek textarea-sleek" placeholder="Add details (optional)..."></textarea>';
497        $html .= '</div>';
498
499        // Color picker
500        $html .= '<div class="form-field">';
501        $html .= '<label class="field-label">�� Color</label>';
502        $html .= '<div class="color-picker-container">';
503        $html .= '<input type="color" id="event-color-' . $calId . '" name="color" value="#3498db" class="input-color-sleek">';
504        $html .= '<span class="color-label">Choose event color</span>';
505        $html .= '</div>';
506        $html .= '</div>';
507
508        // Action buttons
509        $html .= '<div class="dialog-actions-sleek">';
510        $html .= '<button type="button" class="btn-sleek btn-cancel-sleek" onclick="closeEventDialog(\'' . $calId . '\')">Cancel</button>';
511        $html .= '<button type="submit" class="btn-sleek btn-save-sleek">�� Save</button>';
512        $html .= '</div>';
513
514        $html .= '</form>';
515        $html .= '</div>';
516        $html .= '</div>';
517
518        return $html;
519    }
520
521    private function renderDescription($description) {
522        if (empty($description)) {
523            return '';
524        }
525
526        // Convert newlines to <br> for basic formatting
527        $rendered = nl2br($description);
528
529        // Convert DokuWiki image syntax {{image.jpg}} to HTML
530        $rendered = preg_replace_callback(
531            '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/',
532            function($matches) {
533                $imagePath = trim($matches[1]);
534                $alt = isset($matches[2]) ? trim($matches[2]) : '';
535
536                // Handle external URLs (http:// or https://)
537                if (preg_match('/^https?:\/\//', $imagePath)) {
538                    return '<img src="' . htmlspecialchars($imagePath) . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
539                }
540
541                // Handle internal DokuWiki images
542                $imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
543                return '<img src="' . $imageUrl . '" alt="' . htmlspecialchars($alt) . '" class="event-image" />';
544            },
545            $rendered
546        );
547
548        // Convert DokuWiki link syntax [[link|text]] to HTML
549        $rendered = preg_replace_callback(
550            '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/',
551            function($matches) {
552                $link = trim($matches[1]);
553                $text = isset($matches[2]) ? trim($matches[2]) : $link;
554
555                // Handle external URLs
556                if (preg_match('/^https?:\/\//', $link)) {
557                    return '<a href="' . htmlspecialchars($link) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>';
558                }
559
560                // Handle internal DokuWiki links
561                $wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($link);
562                return '<a href="' . $wikiUrl . '">' . htmlspecialchars($text) . '</a>';
563            },
564            $rendered
565        );
566
567        // Convert markdown-style links [text](url) to HTML
568        $rendered = preg_replace_callback(
569            '/\[([^\]]+)\]\(([^)]+)\)/',
570            function($matches) {
571                $text = trim($matches[1]);
572                $url = trim($matches[2]);
573
574                if (preg_match('/^https?:\/\//', $url)) {
575                    return '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($text) . '</a>';
576                }
577
578                return '<a href="' . htmlspecialchars($url) . '">' . htmlspecialchars($text) . '</a>';
579            },
580            $rendered
581        );
582
583        // Convert plain URLs to clickable links
584        $rendered = preg_replace_callback(
585            '/(https?:\/\/[^\s<]+)/',
586            function($matches) {
587                $url = $matches[1];
588                return '<a href="' . htmlspecialchars($url) . '" target="_blank" rel="noopener noreferrer">' . htmlspecialchars($url) . '</a>';
589            },
590            $rendered
591        );
592
593        // Allow basic HTML tags (bold, italic, strong, em, u, code)
594        // Already in the description, just pass through
595
596        return $rendered;
597    }
598
599    private function loadEvents($namespace, $year, $month) {
600        $dataDir = DOKU_INC . 'data/meta/';
601        if ($namespace) {
602            $dataDir .= str_replace(':', '/', $namespace) . '/';
603        }
604        $dataDir .= 'calendar/';
605
606        $eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
607
608        if (file_exists($eventFile)) {
609            $json = file_get_contents($eventFile);
610            return json_decode($json, true);
611        }
612
613        return array();
614    }
615}
616