/path/to/image.jpg|Caption text}} * */ class syntax_plugin_luxtools_image extends SyntaxPlugin { /** @inheritdoc */ public function getType() { return 'substition'; } /** @inheritdoc */ public function getPType() { return 'block'; } /** @inheritdoc */ public function getSort() { return 315; // Same as imagebox plugin } /** @inheritdoc */ public function connectTo($mode) { $this->Lexer->addSpecialPattern('\{\{image>.+?\}\}', $mode, 'plugin_luxtools_image'); } /** @inheritdoc */ public function handle($match, $state, $pos, \Doku_Handler $handler) { // Remove the leading {{image> and trailing }} $match = substr($match, strlen('{{image>'), -2); // Split by | into: path, caption, options // Format: {{image>path|caption|options}} $parts = explode('|', $match, 3); $pathPart = trim($parts[0]); $caption = isset($parts[1]) ? trim($parts[1]) : ''; $optionStr = isset($parts[2]) ? trim($parts[2]) : ''; // Parse options from third part (e.g., "200x150&right") $width = null; $height = null; $align = null; if ($optionStr !== '') { $optionParts = explode('&', $optionStr); foreach ($optionParts as $param) { $param = trim($param); if ($param === '') continue; if (in_array($param, ['left', 'right', 'center'], true)) { $align = $param; } elseif (preg_match('/^(\d+)(?:x(\d+))?$/', $param, $m)) { $width = (int)$m[1]; if (isset($m[2]) && $m[2] !== '') { $height = (int)$m[2]; } } } } $isRemote = ThumbnailHelper::isRemoteUrl($pathPart); $path = $isRemote ? $pathPart : Path::cleanPath($pathPart, false); return [ 'path' => $path, 'is_remote' => $isRemote, 'caption' => $caption, 'align' => $align, 'width' => $width, 'height' => $height, ]; } /** @inheritdoc */ public function render($format, \Doku_Renderer $renderer, $data) { if ($data === false || !is_array($data)) { return false; } if ($format !== 'xhtml') { // For non-XHTML formats, render caption as text if available. if (!empty($data['caption'])) { $renderer->cdata('[Image: ' . $data['caption'] . ']'); } return true; } // Apply default settings if not explicitly specified if ($data['width'] === null) { $data['width'] = (int)$this->getConf('default_image_width'); if ($data['width'] <= 0) $data['width'] = 250; } if ($data['align'] === null) { $data['align'] = (string)$this->getConf('default_image_align'); if (!in_array($data['align'], ['left', 'right', 'center'], true)) { $data['align'] = 'right'; } } if (!empty($data['is_remote'])) { if (empty($data['path']) || !ThumbnailHelper::isRemoteUrl($data['path'])) { $renderer->cdata('[n/a: Invalid URL]'); return true; } // Remote images: link directly, no proxying or thumbnailing $thumb = [ 'url' => $data['path'], 'isFinal' => true, 'thumbUrl' => $data['path'], ]; $this->renderImageBox($renderer, $thumb, $data['path'], $data); return true; } try { $pathHelper = new Path($this->buildPathConfigWithBlobs()); // Use addTrailingSlash=false since this is a file path, not a directory $pathInfo = $pathHelper->getPathInfo($data['path'], false); } catch (\Exception $e) { $renderer->cdata('[n/a: ' . $this->getLang('error_outsidejail') . ']'); return true; } $fullPath = $pathInfo['root'] . $pathInfo['local']; // Verify the file exists and is an image if (!is_file($fullPath)) { $renderer->cdata('[n/a: File not found]'); return true; } // Check if it's an image try { [, $mime,] = mimetype($fullPath, false); } catch (\Throwable $e) { $mime = null; } if (!is_string($mime) || !str_starts_with($mime, 'image/')) { $renderer->cdata('[n/a: Not an image]'); return true; } // Get thumbnail from helper - it handles everything global $ID; $placeholderId = trim((string)$this->getConf('thumb_placeholder')); $thumb = ThumbnailHelper::getThumbnail( $pathInfo['root'], $pathInfo['local'], $ID, $data['width'] ?? 250, $data['height'] ?? ($data['width'] ?? 250), 80, $placeholderId !== '' ? $placeholderId : null ); // Build full-size URL for linking $fullUrl = $this->buildImageUrl($pathInfo, null, null, false); $this->renderImageBox($renderer, $thumb, $fullUrl, $data); return true; } /** * Build the file.php URL for a local image. * * @param array $pathInfo Path info array from Path helper * @param int|null $width Optional width * @param int|null $height Optional height * @param bool $thumbnail Whether to generate a thumbnail * @return string */ protected function buildImageUrl(array $pathInfo, ?int $width, ?int $height, bool $thumbnail): string { global $ID; $params = [ 'root' => $pathInfo['root'], 'file' => $pathInfo['local'], 'id' => $ID, ]; if ($thumbnail && ($width !== null || $height !== null)) { $params['thumb'] = 1; $params['q'] = 80; } if ($width !== null) { $params['w'] = $width; } if ($height !== null) { $params['h'] = $height; } return DOKU_BASE . 'lib/plugins/luxtools/file.php?' . http_build_query($params, '', '&'); } /** * Build a path configuration string, adding the blobs alias if available. */ protected function buildPathConfigWithBlobs(): string { $pathConfig = (string)$this->getConf('paths'); $blobsRoot = $this->resolveBlobsRoot(); if ($blobsRoot !== '') { $pathConfig = rtrim($pathConfig) . "\n" . $blobsRoot . "\nA> blobs"; } return $pathConfig; } /** * Resolve the current page's pagelink folder for the blobs alias. */ protected function resolveBlobsRoot(): string { static $cached = []; global $ID; $pageId = is_string($ID) ? $ID : ''; if ($pageId === '') return ''; if (function_exists('cleanID')) { $pageId = (string)cleanID($pageId); } if ($pageId === '') return ''; if (isset($cached[$pageId])) { return (string)$cached[$pageId]; } $depth = (int)$this->getConf('pagelink_search_depth'); if ($depth < 0) $depth = 0; $pageLink = new PageLink((string)$this->getConf('paths'), $depth); $uuid = $pageLink->getPageUuid($pageId); if ($uuid === null) { $cached[$pageId] = ''; return ''; } $linkInfo = $pageLink->resolveUuid($uuid); $folder = $linkInfo['folder'] ?? ''; if (!is_string($folder) || $folder === '') { $cached[$pageId] = ''; return ''; } $cached[$pageId] = $folder; return $folder; } /** * Render the imagebox HTML. * * @param \Doku_Renderer $renderer * @param array $thumb Thumbnail info from ThumbnailHelper::getThumbnail() * @param string $fullUrl URL for the full-size image (on click) * @param array $data Parsed data from handle() */ protected function renderImageBox(\Doku_Renderer $renderer, array $thumb, string $fullUrl, array $data): void { $align = $data['align'] ?? 'right'; $caption = $data['caption'] ?? ''; $width = $data['width']; $height = $data['height']; // Alignment class $alignClass = 'tright'; // default if ($align === 'left') { $alignClass = 'tleft'; } elseif ($align === 'center') { $alignClass = 'tcenter'; } // Build width style for the outer container $outerStyle = ''; if ($width !== null) { // Add a few pixels for border/padding $outerStyle = ' style="width: ' . ($width + 10) . 'px;"'; } // Use thumbnail metadata from helper $dataThumbAttr = $thumb['isFinal'] ? '' : ' data-thumb-src="' . hsc($thumb['thumbUrl']) . '"'; // Build image attributes $imgAttrs = 'class="media luxtools-thumb" loading="lazy" decoding="async"'; if ($width !== null) { $imgAttrs .= ' width="' . (int)$width . '"'; } if ($height !== null) { $imgAttrs .= ' height="' . (int)$height . '"'; } $imgAttrs .= ' alt="' . hsc($caption) . '"'; /** @var \Doku_Renderer_xhtml $renderer */ $renderer->doc .= '