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