';
$html .= '' . $currentDay . '';
if ($hasEvents) {
// Sort events by time (no time first, then by time)
$sortedEvents = $eventRanges[$dateKey];
usort($sortedEvents, function($a, $b) {
$timeA = isset($a['time']) ? $a['time'] : '';
$timeB = isset($b['time']) ? $b['time'] : '';
// Events without time go first
if (empty($timeA) && !empty($timeB)) return -1;
if (!empty($timeA) && empty($timeB)) return 1;
if (empty($timeA) && empty($timeB)) return 0;
// Sort by time
return strcmp($timeA, $timeB);
});
// Show colored stacked bars for each event
$html .= '
'; // End container
return $html;
}
private function renderEventListContent($events, $calId, $namespace) {
if (empty($events)) {
return '
No events this month
';
}
// Check for time conflicts
$events = $this->checkTimeConflicts($events);
// Sort by date ascending (chronological order - oldest first)
ksort($events);
// Sort events within each day by time
foreach ($events as $dateKey => &$dayEvents) {
usort($dayEvents, function($a, $b) {
$timeA = isset($a['time']) && !empty($a['time']) ? $a['time'] : null;
$timeB = isset($b['time']) && !empty($b['time']) ? $b['time'] : null;
// All-day events (no time) go to the TOP
if ($timeA === null && $timeB !== null) return -1; // A before B
if ($timeA !== null && $timeB === null) return 1; // A after B
if ($timeA === null && $timeB === null) return 0; // Both all-day, equal
// Both have times, sort chronologically
return strcmp($timeA, $timeB);
});
}
unset($dayEvents); // Break reference
// Get today's date for comparison
$today = date('Y-m-d');
$firstFutureEventId = null;
// Helper function to check if event is past (with 15-minute grace period for timed events)
$isEventPast = function($dateKey, $time) use ($today) {
// If event is on a past date, it's definitely past
if ($dateKey < $today) {
return true;
}
// If event is on a future date, it's definitely not past
if ($dateKey > $today) {
return false;
}
// Event is today - check time with grace period
if ($time && $time !== '') {
try {
$currentDateTime = new DateTime();
$eventDateTime = new DateTime($dateKey . ' ' . $time);
// Add 15-minute grace period
$eventDateTime->modify('+15 minutes');
// Event is past if current time > event time + 15 minutes
return $currentDateTime > $eventDateTime;
} catch (Exception $e) {
// If time parsing fails, fall back to date-only comparison
return false;
}
}
// No time specified for today's event, treat as future
return false;
};
// Build HTML for each event - separate past/completed from future
$pastHtml = '';
$futureHtml = '';
$pastCount = 0;
foreach ($events as $dateKey => $dayEvents) {
foreach ($dayEvents as $event) {
// Track first future/today event for auto-scroll
if (!$firstFutureEventId && $dateKey >= $today) {
$firstFutureEventId = isset($event['id']) ? $event['id'] : '';
}
$eventId = isset($event['id']) ? $event['id'] : '';
$title = isset($event['title']) ? htmlspecialchars($event['title']) : 'Untitled';
$timeRaw = isset($event['time']) ? $event['time'] : '';
$time = htmlspecialchars($timeRaw);
$endTime = isset($event['endTime']) ? htmlspecialchars($event['endTime']) : '';
$color = isset($event['color']) ? htmlspecialchars($event['color']) : '#3498db';
$description = isset($event['description']) ? $event['description'] : '';
$isTask = isset($event['isTask']) ? $event['isTask'] : false;
$completed = isset($event['completed']) ? $event['completed'] : false;
$endDate = isset($event['endDate']) ? $event['endDate'] : '';
// Use helper function to determine if event is past (with grace period)
$isPast = $isEventPast($dateKey, $timeRaw);
$isToday = $dateKey === $today;
// Check if event should be in past section
// EXCEPTION: Uncompleted tasks (isTask && !completed) should stay visible even if past
$isPastOrCompleted = ($isPast && (!$isTask || $completed)) || $completed;
if ($isPastOrCompleted) {
$pastCount++;
}
// Determine if task is past due (past date, is task, not completed)
$isPastDue = $isPast && $isTask && !$completed;
// Process description for wiki syntax, HTML, images, and links
$renderedDescription = $this->renderDescription($description);
// Convert to 12-hour format and handle time ranges
$displayTime = '';
if ($time) {
$timeObj = DateTime::createFromFormat('H:i', $time);
if ($timeObj) {
$displayTime = $timeObj->format('g:i A');
// Add end time if present and different from start time
if ($endTime && $endTime !== $time) {
$endTimeObj = DateTime::createFromFormat('H:i', $endTime);
if ($endTimeObj) {
$displayTime .= ' - ' . $endTimeObj->format('g:i A');
}
}
} else {
$displayTime = $time;
}
}
// Format date display with day of week
// Use originalStartDate if this is a multi-month event continuation
$displayDateKey = isset($event['originalStartDate']) ? $event['originalStartDate'] : $dateKey;
$dateObj = new DateTime($displayDateKey);
$displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Jan 24"
// Multi-day indicator
$multiDay = '';
if ($endDate && $endDate !== $displayDateKey) {
$endObj = new DateTime($endDate);
$multiDay = ' → ' . $endObj->format('D, M j');
}
$completedClass = $completed ? ' event-completed' : '';
// Don't grey out past due tasks - they need attention!
$pastClass = ($isPast && !$isPastDue) ? ' event-past' : '';
$pastDueClass = $isPastDue ? ' event-pastdue' : '';
$firstFutureAttr = ($firstFutureEventId === $eventId) ? ' data-first-future="true"' : '';
$eventHtml = '
';
// For past events, hide meta and description (collapsed)
// EXCEPTION: Past due tasks should show their details
if (!$isPast || $isPastDue) {
$eventHtml .= '
';
$eventHtml .= '' . $displayDate . $multiDay;
if ($displayTime) {
$eventHtml .= ' • ' . $displayTime;
}
// Add TODAY badge for today's events OR PAST DUE for uncompleted past tasks
if ($isPastDue) {
$eventHtml .= ' PAST DUE';
} elseif ($isToday) {
$eventHtml .= ' TODAY';
}
// Add namespace badge - ALWAYS show if event has a namespace
$eventNamespace = isset($event['namespace']) ? $event['namespace'] : '';
if (!$eventNamespace && isset($event['_namespace'])) {
$eventNamespace = $event['_namespace']; // Fallback to _namespace for backward compatibility
}
// Show badge if namespace exists and is not empty
if ($eventNamespace && $eventNamespace !== '') {
$eventHtml .= ' ' . htmlspecialchars($eventNamespace) . '';
}
// Add conflict warning if event has time conflicts
if (isset($event['hasConflict']) && $event['hasConflict'] && isset($event['conflictsWith'])) {
$conflictList = [];
foreach ($event['conflictsWith'] as $conflict) {
$conflictText = htmlspecialchars($conflict['title']);
if (!empty($conflict['time'])) {
// Format time range
$startTimeObj = DateTime::createFromFormat('H:i', $conflict['time']);
$startTimeFormatted = $startTimeObj ? $startTimeObj->format('g:i A') : $conflict['time'];
if (!empty($conflict['endTime']) && $conflict['endTime'] !== $conflict['time']) {
$endTimeObj = DateTime::createFromFormat('H:i', $conflict['endTime']);
$endTimeFormatted = $endTimeObj ? $endTimeObj->format('g:i A') : $conflict['endTime'];
$conflictText .= ' (' . $startTimeFormatted . ' - ' . $endTimeFormatted . ')';
} else {
$conflictText .= ' (' . $startTimeFormatted . ')';
}
}
$conflictList[] = $conflictText;
}
$conflictCount = count($event['conflictsWith']);
$conflictJson = htmlspecialchars(json_encode($conflictList), ENT_QUOTES, 'UTF-8');
$eventHtml .= ' ⚠️ ' . $conflictCount . '';
}
$eventHtml .= '';
$eventHtml .= '
';
if ($description) {
$eventHtml .= '
' . $renderedDescription . '
';
}
} else {
// Past events: render with display:none for click-to-expand
$eventHtml .= '
';
// Checkbox for tasks - ON THE FAR RIGHT
if ($isTask) {
$checked = $completed ? 'checked' : '';
$eventHtml .= '';
}
$eventHtml .= '
';
// Add to appropriate section
if ($isPastOrCompleted) {
$pastHtml .= $eventHtml;
} else {
$futureHtml .= $eventHtml;
}
}
}
// Build final HTML with collapsible past events section
$html = '';
// Add collapsible past events section if any exist
if ($pastCount > 0) {
$html .= '
';
} else {
// Calculate today and tomorrow's dates for highlighting
$todayStr = date('Y-m-d');
$tomorrow = date('Y-m-d', strtotime('+1 day'));
foreach ($allEvents as $dateKey => $dayEvents) {
$dateObj = new DateTime($dateKey);
$displayDate = $dateObj->format('D, M j');
// Check if this date is today or tomorrow or past
// Enable highlighting for sidebar mode AND range modes (day, week, month)
$enableHighlighting = $sidebar || !empty($range);
$isToday = $enableHighlighting && ($dateKey === $todayStr);
$isTomorrow = $enableHighlighting && ($dateKey === $tomorrow);
$isPast = $dateKey < $todayStr;
foreach ($dayEvents as $event) {
// Check if this is a task and if it's completed
$isTask = !empty($event['isTask']);
$completed = !empty($event['completed']);
// ALWAYS skip completed tasks UNLESS showchecked is explicitly set
if (!$showchecked && $isTask && $completed) {
continue;
}
// Skip past events that are NOT tasks (only show past due tasks from the past)
if ($isPast && !$isTask) {
continue;
}
// Determine if task is past due (past date, is task, not completed)
$isPastDue = $isPast && $isTask && !$completed;
// Line 1: Header (Title, Time, Date, Namespace)
$todayClass = $isToday ? ' eventlist-simple-today' : '';
$tomorrowClass = $isTomorrow ? ' eventlist-simple-tomorrow' : '';
$pastDueClass = $isPastDue ? ' eventlist-simple-pastdue' : '';
$html .= '
';
return $html;
}
private function renderDescription($description) {
if (empty($description)) {
return '';
}
// Token-based parsing to avoid escaping issues
$rendered = $description;
$tokens = array();
$tokenIndex = 0;
// Convert DokuWiki image syntax {{image.jpg}} to tokens
$pattern = '/\{\{([^}|]+?)(?:\|([^}]+))?\}\}/';
preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$imagePath = trim($match[1]);
$alt = isset($match[2]) ? trim($match[2]) : '';
// Handle external URLs
if (preg_match('/^https?:\/\//', $imagePath)) {
$imageHtml = '';
} else {
// Handle internal DokuWiki images
$imageUrl = DOKU_BASE . 'lib/exe/fetch.php?media=' . rawurlencode($imagePath);
$imageHtml = '';
}
$token = "\x00TOKEN" . $tokenIndex . "\x00";
$tokens[$tokenIndex] = $imageHtml;
$tokenIndex++;
$rendered = str_replace($match[0], $token, $rendered);
}
// Convert DokuWiki link syntax [[link|text]] to tokens
$pattern = '/\[\[([^|\]]+?)(?:\|([^\]]+))?\]\]/';
preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$link = trim($match[1]);
$text = isset($match[2]) ? trim($match[2]) : $link;
// Handle external URLs
if (preg_match('/^https?:\/\//', $link)) {
$linkHtml = '' . htmlspecialchars($text) . '';
} else {
// Handle internal DokuWiki links with section anchors
$parts = explode('#', $link, 2);
$pagePart = $parts[0];
$sectionPart = isset($parts[1]) ? '#' . $parts[1] : '';
$wikiUrl = DOKU_BASE . 'doku.php?id=' . rawurlencode($pagePart) . $sectionPart;
$linkHtml = '' . htmlspecialchars($text) . '';
}
$token = "\x00TOKEN" . $tokenIndex . "\x00";
$tokens[$tokenIndex] = $linkHtml;
$tokenIndex++;
$rendered = str_replace($match[0], $token, $rendered);
}
// Convert markdown-style links [text](url) to tokens
$pattern = '/\[([^\]]+)\]\(([^)]+)\)/';
preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$text = trim($match[1]);
$url = trim($match[2]);
if (preg_match('/^https?:\/\//', $url)) {
$linkHtml = '' . htmlspecialchars($text) . '';
} else {
$linkHtml = '' . htmlspecialchars($text) . '';
}
$token = "\x00TOKEN" . $tokenIndex . "\x00";
$tokens[$tokenIndex] = $linkHtml;
$tokenIndex++;
$rendered = str_replace($match[0], $token, $rendered);
}
// Convert plain URLs to tokens
$pattern = '/(https?:\/\/[^\s<]+)/';
preg_match_all($pattern, $rendered, $matches, PREG_SET_ORDER);
foreach ($matches as $match) {
$url = $match[1];
$linkHtml = '' . htmlspecialchars($url) . '';
$token = "\x00TOKEN" . $tokenIndex . "\x00";
$tokens[$tokenIndex] = $linkHtml;
$tokenIndex++;
$rendered = str_replace($match[0], $token, $rendered);
}
// NOW escape HTML (tokens are protected)
$rendered = htmlspecialchars($rendered);
// Convert newlines to
$rendered = nl2br($rendered);
// DokuWiki text formatting
// Bold: **text** or __text__
$rendered = preg_replace('/\*\*(.+?)\*\*/', '$1', $rendered);
$rendered = preg_replace('/__(.+?)__/', '$1', $rendered);
// Italic: //text//
$rendered = preg_replace('/\/\/(.+?)\/\//', '$1', $rendered);
// Strikethrough: text
$rendered = preg_replace('/<del>(.+?)<\/del>/', '$1', $rendered);
// Monospace: ''text''
$rendered = preg_replace('/''(.+?)''/', '$1', $rendered);
// Subscript: text
$rendered = preg_replace('/<sub>(.+?)<\/sub>/', '$1', $rendered);
// Superscript: text
$rendered = preg_replace('/<sup>(.+?)<\/sup>/', '$1', $rendered);
// Restore tokens
foreach ($tokens as $i => $html) {
$rendered = str_replace("\x00TOKEN" . $i . "\x00", $html, $rendered);
}
return $rendered;
}
private function loadEvents($namespace, $year, $month) {
$dataDir = DOKU_INC . 'data/meta/';
if ($namespace) {
$dataDir .= str_replace(':', '/', $namespace) . '/';
}
$dataDir .= 'calendar/';
$eventFile = $dataDir . sprintf('%04d-%02d.json', $year, $month);
if (file_exists($eventFile)) {
$json = file_get_contents($eventFile);
return json_decode($json, true);
}
return array();
}
private function loadEventsMultiNamespace($namespaces, $year, $month) {
// Check for wildcard pattern (namespace:*)
if (preg_match('/^(.+):\*$/', $namespaces, $matches)) {
$baseNamespace = $matches[1];
return $this->loadEventsWildcard($baseNamespace, $year, $month);
}
// Check for root wildcard (just *)
if ($namespaces === '*') {
return $this->loadEventsWildcard('', $year, $month);
}
// Parse namespace list (semicolon separated)
// e.g., "team:projects;personal;work:tasks" = three namespaces
$namespaceList = array_map('trim', explode(';', $namespaces));
// Load events from all namespaces
$allEvents = array();
foreach ($namespaceList as $ns) {
$ns = trim($ns);
if (empty($ns)) continue;
$events = $this->loadEvents($ns, $year, $month);
// Add namespace tag to each event
foreach ($events as $dateKey => $dayEvents) {
if (!isset($allEvents[$dateKey])) {
$allEvents[$dateKey] = array();
}
foreach ($dayEvents as $event) {
$event['_namespace'] = $ns;
$allEvents[$dateKey][] = $event;
}
}
}
return $allEvents;
}
private function loadEventsWildcard($baseNamespace, $year, $month) {
// Find all subdirectories under the base namespace
$dataDir = DOKU_INC . 'data/meta/';
if ($baseNamespace) {
$dataDir .= str_replace(':', '/', $baseNamespace) . '/';
}
$allEvents = array();
// First, load events from the base namespace itself
if (empty($baseNamespace)) {
// Root wildcard - load from root calendar
$events = $this->loadEvents('', $year, $month);
foreach ($events as $dateKey => $dayEvents) {
if (!isset($allEvents[$dateKey])) {
$allEvents[$dateKey] = array();
}
foreach ($dayEvents as $event) {
$event['_namespace'] = '';
$allEvents[$dateKey][] = $event;
}
}
} else {
$events = $this->loadEvents($baseNamespace, $year, $month);
foreach ($events as $dateKey => $dayEvents) {
if (!isset($allEvents[$dateKey])) {
$allEvents[$dateKey] = array();
}
foreach ($dayEvents as $event) {
$event['_namespace'] = $baseNamespace;
$allEvents[$dateKey][] = $event;
}
}
}
// Recursively find all subdirectories
$this->findSubNamespaces($dataDir, $baseNamespace, $year, $month, $allEvents);
return $allEvents;
}
private function findSubNamespaces($dir, $baseNamespace, $year, $month, &$allEvents) {
if (!is_dir($dir)) return;
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$path = $dir . $item;
if (is_dir($path) && $item !== 'calendar') {
// This is a namespace directory
$namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
// Load events from this namespace
$events = $this->loadEvents($namespace, $year, $month);
foreach ($events as $dateKey => $dayEvents) {
if (!isset($allEvents[$dateKey])) {
$allEvents[$dateKey] = array();
}
foreach ($dayEvents as $event) {
$event['_namespace'] = $namespace;
$allEvents[$dateKey][] = $event;
}
}
// Recurse into subdirectories
$this->findSubNamespaces($path . '/', $namespace, $year, $month, $allEvents);
}
}
}
private function getAllNamespaces() {
$dataDir = DOKU_INC . 'data/meta/';
$namespaces = [];
// Scan for namespaces that have calendar data
$this->scanForCalendarNamespaces($dataDir, '', $namespaces);
// Sort alphabetically
sort($namespaces);
return $namespaces;
}
private function scanForCalendarNamespaces($dir, $baseNamespace, &$namespaces) {
if (!is_dir($dir)) return;
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..') continue;
$path = $dir . $item;
if (is_dir($path)) {
// Check if this directory has a calendar subdirectory with data
$calendarDir = $path . '/calendar/';
if (is_dir($calendarDir)) {
// Check if there are any JSON files in the calendar directory
$jsonFiles = glob($calendarDir . '*.json');
if (!empty($jsonFiles)) {
// This namespace has calendar data
$namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
$namespaces[] = $namespace;
}
}
// Recurse into subdirectories
$namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
$this->scanForCalendarNamespaces($path . '/', $namespace, $namespaces);
}
}
}
/**
* Render new sidebar widget - Week at a glance itinerary (200px wide)
*/
private function renderSidebarWidget($events, $namespace, $calId) {
if (empty($events)) {
return '
No events this week
';
}
// Get important namespaces from config
$configFile = DOKU_PLUGIN . 'calendar/sync_config.php';
$importantNsList = ['important']; // default
if (file_exists($configFile)) {
$config = include $configFile;
if (isset($config['important_namespaces']) && !empty($config['important_namespaces'])) {
$importantNsList = array_map('trim', explode(',', $config['important_namespaces']));
}
}
// Calculate date ranges
$todayStr = date('Y-m-d');
$tomorrowStr = date('Y-m-d', strtotime('+1 day'));
$weekStart = date('Y-m-d', strtotime('monday this week'));
$weekEnd = date('Y-m-d', strtotime('sunday this week'));
// Group events by category
$todayEvents = [];
$tomorrowEvents = [];
$importantEvents = [];
$weekEvents = []; // For week grid
// Process all events
foreach ($events as $dateKey => $dayEvents) {
// Skip events before this week
if ($dateKey < $weekStart) continue;
// Initialize week grid day if in current week
if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
if (!isset($weekEvents[$dateKey])) {
$weekEvents[$dateKey] = [];
}
}
foreach ($dayEvents as $event) {
// Add to week grid if in week range
if ($dateKey >= $weekStart && $dateKey <= $weekEnd) {
// Pre-render DokuWiki syntax to HTML for JavaScript display
$eventWithHtml = $event;
if (isset($event['title'])) {
$eventWithHtml['title_html'] = $this->renderDokuWikiToHtml($event['title']);
}
if (isset($event['description'])) {
$eventWithHtml['description_html'] = $this->renderDokuWikiToHtml($event['description']);
}
$weekEvents[$dateKey][] = $eventWithHtml;
}
// Categorize for detailed sections
if ($dateKey === $todayStr) {
$todayEvents[] = array_merge($event, ['date' => $dateKey]);
} elseif ($dateKey === $tomorrowStr) {
$tomorrowEvents[] = array_merge($event, ['date' => $dateKey]);
} else {
// Check if this is an important namespace
$eventNs = isset($event['namespace']) ? $event['namespace'] : '';
$isImportant = false;
foreach ($importantNsList as $impNs) {
if ($eventNs === $impNs || strpos($eventNs, $impNs . ':') === 0) {
$isImportant = true;
break;
}
}
// Important events: this week but not today/tomorrow
if ($isImportant && $dateKey >= $weekStart && $dateKey <= $weekEnd) {
$importantEvents[] = array_merge($event, ['date' => $dateKey]);
}
}
}
}
// Start building HTML - Dynamic width with default font
$html = '
';
// Sanitize calId for use in JavaScript variable names (remove dashes)
$jsCalId = str_replace('-', '_', $calId);
// CRITICAL: Add ALL JavaScript FIRST before any HTML that uses it
$html .= '';
// NOW add the header HTML (after JavaScript is defined)
$todayDate = new DateTime();
$displayDate = $todayDate->format('D, M j, Y');
$currentTime = $todayDate->format('g:i:s A');
$html .= '
';
// Get today's date for default event date
$todayStr = date('Y-m-d');
// Thin dark green "Add Event" bar between header and week grid (zero margin, smaller text, text positioned higher)
$html .= '
';
// Event bars (max 3 visible) with glow effect
if ($eventCount > 0) {
$showCount = min($eventCount, 3);
for ($j = 0; $j < $showCount; $j++) {
$event = $events[$j];
$color = isset($event['color']) ? $event['color'] : '#00cc07';
$html .= '';
}
// Show "+N more" if more than 3
if ($eventCount > 3) {
$html .= '
+' . ($eventCount - 3) . '
';
}
}
$html .= '
';
}
$html .= '
';
// Add container for selected day events display (with unique ID)
$html .= '
';
$html .= '
';
$html .= '';
$html .= '✕';
$html .= '
';
$html .= '';
$html .= '
';
// Add JavaScript for day selection with event data
$html .= '';
return $html;
}
/**
* Render a sidebar section (Today/Tomorrow/Important) - Matrix themed with colored borders
*/
private function renderSidebarSection($title, $events, $accentColor, $calId) {
// Keep the original accent colors for borders
$borderColor = $accentColor;
// Show date for Important Events section
$showDate = ($title === 'Important Events');
$html = '
';
// Section header with accent color background - smaller, not all caps
$html .= '
';
// Date display BELOW event name for Important events
if ($showDate && $date) {
$dateObj = new DateTime($date);
$displayDate = $dateObj->format('D, M j'); // e.g., "Mon, Feb 10"
$html .= '
' . htmlspecialchars($displayDate) . '
';
}
$html .= '
';
$html .= '
';
return $html;
}
/**
* Format time display (12-hour format with optional end time)
*/
private function formatTimeDisplay($startTime, $endTime = '') {
// Convert start time
list($hour, $minute) = explode(':', $startTime);
$hour = (int)$hour;
$ampm = $hour >= 12 ? 'PM' : 'AM';
$displayHour = $hour % 12;
if ($displayHour === 0) $displayHour = 12;
$display = $displayHour . ':' . $minute . ' ' . $ampm;
// Add end time if provided
if ($endTime && $endTime !== '') {
list($endHour, $endMinute) = explode(':', $endTime);
$endHour = (int)$endHour;
$endAmpm = $endHour >= 12 ? 'PM' : 'AM';
$endDisplayHour = $endHour % 12;
if ($endDisplayHour === 0) $endDisplayHour = 12;
$display .= '-' . $endDisplayHour . ':' . $endMinute . ' ' . $endAmpm;
}
return $display;
}
/**
* Render DokuWiki syntax to HTML
* Converts **bold**, //italic//, [[links]], etc. to HTML
*/
private function renderDokuWikiToHtml($text) {
if (empty($text)) return '';
// Use DokuWiki's parser to render the text
$instructions = p_get_instructions($text);
// Render instructions to XHTML
$xhtml = p_render('xhtml', $instructions, $info);
// Remove surrounding
tags if present (we're rendering inline)
$xhtml = preg_replace('/^
(.*)<\/p>$/s', '$1', trim($xhtml));
return $xhtml;
}
// Keep old scanForNamespaces for backward compatibility (not used anymore)
private function scanForNamespaces($dir, $baseNamespace, &$namespaces) {
if (!is_dir($dir)) return;
$items = scandir($dir);
foreach ($items as $item) {
if ($item === '.' || $item === '..' || $item === 'calendar') continue;
$path = $dir . $item;
if (is_dir($path)) {
$namespace = $baseNamespace ? $baseNamespace . ':' . $item : $item;
$namespaces[] = $namespace;
$this->scanForNamespaces($path . '/', $namespace, $namespaces);
}
}
}
}