register_hook( "TPL_METAHEADER_OUTPUT", "BEFORE", $this, "addScripts", ); $controller->register_hook( "RENDERER_CONTENT_POSTPROCESS", "BEFORE", $this, "autoLinkChronologicalDates", ); $controller->register_hook( "RENDERER_CONTENT_POSTPROCESS", "BEFORE", $this, "appendChronologicalDayEvents", ); $controller->register_hook( "RENDERER_CONTENT_POSTPROCESS", "BEFORE", $this, "appendChronologicalDayPhotos", ); $controller->register_hook( "COMMON_PAGETPL_LOAD", "BEFORE", $this, "prefillChronologicalDayTemplate", ); $controller->register_hook( "TPL_ACT_RENDER", "BEFORE", $this, "renderVirtualChronologicalDayPage", ); $controller->register_hook( "CSS_STYLES_INCLUDED", "BEFORE", $this, "addTemporaryInputStyles", ); $controller->register_hook( "AJAX_CALL_UNKNOWN", "BEFORE", $this, "handleCalendarWidgetAjax", ); $controller->register_hook( "TOOLBAR_DEFINE", "AFTER", $this, "addToolbarButton", ); } /** * Add plugin JavaScript files in a deterministic order. * * @param Event $event * @param mixed $param * @return void */ public function addScripts(Event $event, $param) { $plugin = $this->getPluginName(); $base = DOKU_BASE . "lib/plugins/$plugin/js/"; $scripts = [ "lightbox.js", "gallery-thumbnails.js", "open-service.js", "scratchpads.js", "date-fix.js", "page-link.js", "linkfavicon.js", "calendar-widget.js", "main.js", ]; foreach ($scripts as $script) { $event->data["script"][] = [ "type" => "text/javascript", "src" => $base . $script, ]; } } /** * Serve server-rendered calendar widget HTML for month navigation. * * @param Event $event * @param mixed $param * @return void */ public function handleCalendarWidgetAjax(Event $event, $param) { if ($event->data !== 'luxtools_calendar_month') return; $event->preventDefault(); $event->stopPropagation(); global $INPUT; $year = (int)$INPUT->int('year'); $month = (int)$INPUT->int('month'); $baseNs = trim((string)$INPUT->str('base')); if ($baseNs === '') { $baseNs = 'chronological'; } if (!ChronologicalCalendarWidget::isValidMonth($year, $month)) { http_status(400); echo 'Invalid month'; return; } $html = ChronologicalCalendarWidget::render($year, $month, $baseNs); if ($html === '') { http_status(500); echo 'Calendar rendering failed'; return; } header('Content-Type: text/html; charset=utf-8'); echo $html; } /** * Include temporary global input styling via css.php so @ini_* placeholders resolve. * * @param Event $event * @param mixed $param * @return void */ public function addTemporaryInputStyles(Event $event, $param) { if (!isset($event->data['mediatype']) || $event->data['mediatype'] !== 'screen') { return; } if (!isset($event->data['files']) || !is_array($event->data['files'])) { return; } $plugin = $this->getPluginName(); $event->data['files'][DOKU_PLUGIN . $plugin . '/temp-input-colors.css'] = DOKU_BASE . 'lib/plugins/' . $plugin . '/'; } /** * Auto-link strict ISO dates (YYYY-MM-DD) in rendered XHTML text nodes. * * Excludes content inside tags where links should not be altered. * * @param Event $event * @param mixed $param * @return void */ public function autoLinkChronologicalDates(Event $event, $param) { if (!is_array($event->data)) return; $mode = (string)($event->data[0] ?? ''); if ($mode !== 'xhtml') return; $doc = $event->data[1] ?? null; if (!is_string($doc) || $doc === '') return; if (!preg_match('/\d{4}-\d{2}-\d{2}/', $doc)) return; $event->data[1] = ChronologicalDateAutoLinker::linkHtml($doc); } /** * Prefill new chronological day pages with a German date headline. * * @param Event $event * @param mixed $param * @return void */ public function prefillChronologicalDayTemplate(Event $event, $param) { if (!is_array($event->data)) return; $id = (string)($event->data['id'] ?? ''); if ($id === '') return; if (function_exists('cleanID')) { $id = (string)cleanID($id); } if ($id === '') return; if (!ChronoID::isDayId($id)) return; $template = ChronologicalDayTemplate::buildForDayId($id); if ($template === null || $template === '') return; $event->data['tpl'] = $template; $event->data['tplfile'] = ''; $event->data['doreplace'] = false; } /** * Append matching date-prefixed photos to chronological day page output. * * @param Event $event * @param mixed $param * @return void */ public function appendChronologicalDayPhotos(Event $event, $param) { if (self::$internalRenderInProgress) return; if (!is_array($event->data)) return; $mode = (string)($event->data[0] ?? ''); if ($mode !== 'xhtml') return; global $ACT; if (!is_string($ACT) || $ACT !== 'show') return; $doc = $event->data[1] ?? null; if (!is_string($doc)) return; if (str_contains($doc, 'luxtools-chronological-photos')) return; global $ID; $id = is_string($ID) ? $ID : ''; if ($id === '') return; if (function_exists('cleanID')) { $id = (string)cleanID($id); } if ($id === '') return; $parts = ChronoID::parseDayId($id); if ($parts === null) return; if (!function_exists('page_exists') || !page_exists($id)) return; $basePath = trim((string)$this->getConf('image_base_path')); if ($basePath === '') return; $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']); if (!$this->hasAnyChronologicalPhotos($dateIso)) return; $photosHtml = $this->renderChronologicalPhotosMacro($dateIso); if ($photosHtml === '') return; $event->data[1] = $doc . $photosHtml; } /** * Append local calendar events to existing chronological day pages. * * @param Event $event * @param mixed $param * @return void */ public function appendChronologicalDayEvents(Event $event, $param) { static $appendInProgress = false; if ($appendInProgress) return; if (self::$internalRenderInProgress) return; if (!is_array($event->data)) return; $mode = (string)($event->data[0] ?? ''); if ($mode !== 'xhtml') return; global $ACT; if (!is_string($ACT) || $ACT !== 'show') return; $doc = $event->data[1] ?? null; if (!is_string($doc)) return; if (str_contains($doc, 'luxtools-chronological-events')) return; global $ID; $id = is_string($ID) ? $ID : ''; if ($id === '') return; if (function_exists('cleanID')) { $id = (string)cleanID($id); } if ($id === '') return; $parts = ChronoID::parseDayId($id); if ($parts === null) return; if (!function_exists('page_exists') || !page_exists($id)) return; $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']); $appendInProgress = true; try { $eventsHtml = $this->renderChronologicalEventsHtml($dateIso); } finally { $appendInProgress = false; } if ($eventsHtml === '') return; $event->data[1] = $doc . $eventsHtml; } /** * Render chronological day photos using existing {{images>...}} syntax. * * @param string $dateIso * @return string */ protected function renderChronologicalPhotosMacro(string $dateIso): string { $syntax = $this->buildChronologicalImagesSyntax($dateIso); if ($syntax === '') return ''; if (self::$internalRenderInProgress) return ''; self::$internalRenderInProgress = true; try { $info = ['cache' => false]; $instructions = p_get_instructions($syntax); $galleryHtml = (string)p_render('xhtml', $instructions, $info); } finally { self::$internalRenderInProgress = false; } if ($galleryHtml === '') return ''; $title = (string)$this->getLang('chronological_photos_title'); if ($title === '') $title = 'Photos'; return '
' . '

' . hsc($title) . '

' . $galleryHtml . '
'; } /** * Build {{images>...}} syntax for a given day. * * @param string $dateIso * @return string */ protected function buildChronologicalImagesSyntax(string $dateIso): string { $basePath = trim((string)$this->getConf('image_base_path')); if ($basePath === '') return ''; $base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath); if (!is_dir($base) || !is_readable($base)) return ''; $yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/'; $targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base; return '{{images>' . $targetDir . $dateIso . '*&recursive=0}}'; } /** * Render a virtual day page for missing chronological day IDs. * * Shows a German date heading and existing day photos (if any) without creating the page. * * @param Event $event * @param mixed $param * @return void */ public function renderVirtualChronologicalDayPage(Event $event, $param) { if (!is_string($event->data) || $event->data !== 'show') return; global $ID; $id = is_string($ID) ? $ID : ''; if ($id === '') return; if (function_exists('cleanID')) { $id = (string)cleanID($id); } if ($id === '') return; if (!ChronoID::isDayId($id)) return; if (function_exists('page_exists') && page_exists($id)) return; $wikiText = ChronologicalDayTemplate::buildForDayId($id) ?? ''; if ($wikiText === '') return; $parts = ChronoID::parseDayId($id); $extraHtml = ''; if ($parts !== null) { $dateIso = sprintf('%04d-%02d-%02d', $parts['year'], $parts['month'], $parts['day']); $eventsHtml = $this->renderChronologicalEventsHtml($dateIso); if ($eventsHtml !== '') { $extraHtml .= $eventsHtml; } if ($this->hasAnyChronologicalPhotos($dateIso)) { $photosHtml = $this->renderChronologicalPhotosMacro($dateIso); if ($photosHtml !== '') { $extraHtml .= $photosHtml; } } } $editUrl = function_exists('wl') ? (string)wl($id, ['do' => 'edit']) : ''; $createLinkHtml = ''; if ($editUrl !== '') { $label = (string)$this->getLang('btn_create'); if ($label === '') $label = 'Create this page'; $createLinkHtml = '

✎ ' . hsc($label) . '

'; } $info = ['cache' => false]; $instructions = p_get_instructions($wikiText); $html = (string)p_render('xhtml', $instructions, $info); echo $html . $createLinkHtml . $extraHtml; $event->preventDefault(); $event->stopPropagation(); } /** * Check if there is at least one date-prefixed image for the given day. * * @param string $dateIso * @return bool */ protected function hasAnyChronologicalPhotos(string $dateIso): bool { if (!ChronoID::isIsoDate($dateIso)) return false; $basePath = trim((string)$this->getConf('image_base_path')); if ($basePath === '') return false; $base = \dokuwiki\plugin\luxtools\Path::cleanPath($basePath); if (!is_dir($base) || !is_readable($base)) return false; $yearDir = rtrim($base, '/') . '/' . substr($dateIso, 0, 4) . '/'; $targetDir = (is_dir($yearDir) && is_readable($yearDir)) ? $yearDir : $base; $pattern = rtrim($targetDir, '/') . '/' . $dateIso . '*'; $matches = glob($pattern) ?: []; foreach ($matches as $match) { if (!is_file($match)) continue; $ext = strtolower(pathinfo($match, PATHINFO_EXTENSION)); if (in_array($ext, ['jpg', 'jpeg', 'png', 'gif', 'webp'], true)) { return true; } } return false; } /** * Render local calendar events section for a given date. * * @param string $dateIso * @return string */ protected function renderChronologicalEventsHtml(string $dateIso): string { $icsConfig = (string)$this->getConf('calendar_ics_files'); if (trim($icsConfig) === '') return ''; $events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso); if ($events === []) return ''; $title = (string)$this->getLang('chronological_events_title'); if ($title === '') $title = 'Events'; $items = ''; foreach ($events as $entry) { $summary = trim((string)($entry['summary'] ?? '')); if ($summary === '') $summary = '(ohne Titel)'; $time = trim((string)($entry['time'] ?? '')); $startIso = trim((string)($entry['startIso'] ?? '')); $isAllDay = (bool)($entry['allDay'] ?? false); if ($isAllDay || $time === '') { $items .= '
  • ' . hsc($summary) . '
  • '; } else { $timeHtml = ''; $items .= '
  • ' . $timeHtml . ' - ' . hsc($summary) . '
  • '; } } if ($items === '') return ''; $html = ''; return '
    ' . '

    ' . hsc($title) . '

    ' . $html . '
    '; } /** * Build wiki bullet list for local calendar events. * * @param string $dateIso * @return string */ protected function buildChronologicalEventsWiki(string $dateIso): string { $icsConfig = (string)$this->getConf('calendar_ics_files'); if (trim($icsConfig) === '') return ''; $events = ChronologicalIcsEvents::eventsForDate($icsConfig, $dateIso); if ($events === []) return ''; $lines = []; foreach ($events as $event) { $summary = trim((string)($event['summary'] ?? '')); if ($summary === '') $summary = '(ohne Titel)'; $summary = str_replace(["\n", "\r"], ' ', $summary); $time = trim((string)($event['time'] ?? '')); if ((bool)($event['allDay'] ?? false) || $time === '') { $lines[] = ' * ' . $summary; } else { $lines[] = ' * ' . $time . ' - ' . $summary; } } return implode("\n", $lines); } /** * Add custom toolbar button for code blocks. * * @param Event $event * @param mixed $param * @return void */ public function addToolbarButton(Event $event, $param) { $event->data[] = [ "type" => "format", "title" => $this->getLang("toolbar_code_title"), "icon" => "../../plugins/luxtools/images/code.png", "key" => "C", "open" => "", "sample" => $this->getLang("toolbar_code_sample"), "close" => "", "block" => false, ]; // Date Fix: normalize selected timestamp $event->data[] = [ "type" => "LuxtoolsDatefix", "title" => $this->getLang("toolbar_datefix_title"), "icon" => "../../plugins/luxtools/images/date-fix.svg", "key" => "t", "block" => false, ]; // Date Fix All: normalize all timestamps on page $event->data[] = [ "type" => "LuxtoolsDatefixAll", "title" => $this->getLang("toolbar_datefix_all_title"), "icon" => "../../plugins/luxtools/images/date-fix-all.svg", "block" => false, ]; } }