582 lines
18 KiB
PHP
582 lines
18 KiB
PHP
<?php
|
|
|
|
use dokuwiki\Extension\ActionPlugin;
|
|
use dokuwiki\Extension\Event;
|
|
use dokuwiki\Extension\EventHandler;
|
|
use dokuwiki\plugin\luxtools\ChronoID;
|
|
use dokuwiki\plugin\luxtools\ChronologicalCalendarWidget;
|
|
use dokuwiki\plugin\luxtools\ChronologicalDateAutoLinker;
|
|
use dokuwiki\plugin\luxtools\ChronologicalDayTemplate;
|
|
use dokuwiki\plugin\luxtools\ChronologicalIcsEvents;
|
|
require_once(__DIR__ . '/autoload.php');
|
|
|
|
/**
|
|
* luxtools action plugin: register JS assets.
|
|
*/
|
|
class action_plugin_luxtools extends ActionPlugin
|
|
{
|
|
/** @var bool Guard to prevent postprocess appenders during internal renders */
|
|
protected static $internalRenderInProgress = false;
|
|
|
|
/** @inheritdoc */
|
|
public function register(EventHandler $controller)
|
|
{
|
|
$controller->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 '<div class="luxtools-plugin luxtools-chronological-photos">'
|
|
. '<h2>' . hsc($title) . '</h2>'
|
|
. $galleryHtml
|
|
. '</div>';
|
|
}
|
|
|
|
/**
|
|
* 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 = '<p><a href="' . hsc($editUrl) . '">✎ ' . hsc($label) . '</a></p>';
|
|
}
|
|
|
|
$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 .= '<li>' . hsc($summary) . '</li>';
|
|
} else {
|
|
$timeHtml = '<span class="luxtools-event-time"';
|
|
if ($startIso !== '') {
|
|
$timeHtml .= ' data-luxtools-start="' . hsc($startIso) . '"';
|
|
}
|
|
$timeHtml .= '>' . hsc($time) . '</span>';
|
|
$items .= '<li>' . $timeHtml . ' - ' . hsc($summary) . '</li>';
|
|
}
|
|
}
|
|
|
|
if ($items === '') return '';
|
|
$html = '<ul>' . $items . '</ul>';
|
|
|
|
return '<div class="luxtools-plugin luxtools-chronological-events">'
|
|
. '<h2>' . hsc($title) . '</h2>'
|
|
. $html
|
|
. '</div>';
|
|
}
|
|
|
|
/**
|
|
* 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" => "<code>",
|
|
"sample" => $this->getLang("toolbar_code_sample"),
|
|
"close" => "</code>",
|
|
"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,
|
|
];
|
|
}
|
|
}
|