Add single image display.

This commit is contained in:
2026-01-19 09:16:02 +01:00
parent 95a0e94b4a
commit 8aa022feff
14 changed files with 390 additions and 59 deletions

View File

@@ -82,13 +82,6 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $INPUT;
// Do not allow the syntax in discussion plugin comments
if (!$this->getConf('allow_in_comments') && $INPUT->has('comment')) {
return false;
}
$keyword = $this->getSyntaxKeyword();
$match = substr($match, strlen('{{' . $keyword . '>'), -2);
[$path, $flags] = array_pad(explode('&', $match, 2), 2, '');
@@ -161,6 +154,12 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
*/
protected function parseFlags(string $flags): array
{
// Parse default table columns setting.
// Format: comma-separated list of column names (name, size, date).
$tableColumns = strtolower(trim((string)$this->getConf('default_tablecolumns')));
$defaultShowSize = str_contains($tableColumns, 'size') ? 1 : (int)$this->getConf('default_showsize');
$defaultShowDate = str_contains($tableColumns, 'date') ? 1 : (int)$this->getConf('default_showdate');
// Base defaults shared by all handlers
$baseDefaults = [
'sort' => (string)$this->getConf('default_sort'),
@@ -172,8 +171,8 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
'titlefile' => (string)$this->getConf('default_titlefile'),
'cache' => (int)$this->getConf('default_cache'),
'randlinks' => (int)$this->getConf('default_randlinks'),
'showsize' => (int)$this->getConf('default_showsize'),
'showdate' => (int)$this->getConf('default_showdate'),
'showsize' => $defaultShowSize,
'showdate' => $defaultShowDate,
'listsep' => (string)$this->getConf('default_listsep'),
'maxheight' => (int)$this->getConf('default_maxheight'),
];

View File

@@ -54,14 +54,10 @@ class syntax_plugin_luxtools_directory extends syntax_plugin_luxtools_abstract
$params['titlefile']
);
if ($items == []) {
$this->renderEmptyState($renderer, 'empty_files');
return true;
}
// Always render as table style
$params['style'] = 'table';
// Render the table even if empty so the "Open Location" link is displayed.
$output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $items);
$output->renderAsFlatTable($params);
return true;

View File

@@ -41,9 +41,9 @@ class syntax_plugin_luxtools_files extends syntax_plugin_luxtools_abstract
$params['titlefile']
);
if ($result == []) {
$this->renderEmptyState($renderer, 'empty_files');
return true;
// For table style, pass the base directory as openlocation so the "Open Location" link is displayed.
if ($params['style'] === 'table') {
$params['openlocation'] = $pathInfo['root'] . $pathInfo['local'];
}
$output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $result);

242
syntax/image.php Normal file
View File

@@ -0,0 +1,242 @@
<?php
use dokuwiki\Extension\SyntaxPlugin;
use dokuwiki\plugin\luxtools\Path;
require_once(__DIR__ . '/../autoload.php');
/**
* luxtools Plugin: Image syntax.
*
* Renders a single image in an imagebox (similar to Wikipedia-style image boxes).
* Syntax: {{image>/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 path and caption by |
$parts = explode('|', $match, 2);
$pathPart = trim($parts[0]);
$caption = isset($parts[1]) ? trim($parts[1]) : '';
// Parse optional parameters from path (e.g., /path/image.jpg?200x150&right)
// Supported formats:
// ?200 - width only
// ?200x150 - width and height
// ?left - alignment only (left, right, center)
// ?200&right - width and alignment
// ?200x150&center - full options
$width = null;
$height = null;
$align = 'right'; // default alignment
if (strpos($pathPart, '?') !== false) {
[$pathPart, $paramStr] = explode('?', $pathPart, 2);
$paramParts = explode('&', $paramStr);
foreach ($paramParts as $param) {
$param = trim($param);
if ($param === '') continue;
// Check if it's an alignment keyword
if (in_array($param, ['left', 'right', 'center'], true)) {
$align = $param;
// Check if it's a size specification (digits, optionally with 'x' and more digits)
} elseif (preg_match('/^(\d+)(?:x(\d+))?$/', $param, $m)) {
$width = (int)$m[1];
if (isset($m[2]) && $m[2] !== '') {
$height = (int)$m[2];
}
}
}
}
$path = Path::cleanPath($pathPart, false);
return [
'path' => $path,
'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;
}
try {
$pathHelper = new Path($this->getConf('paths'));
// 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;
}
// Build the image URL using the plugin's file.php endpoint
global $ID;
$imageUrl = $this->buildFileUrl($pathInfo, $data['width'], $data['height']);
// Build full-size URL for linking
$fullUrl = $this->buildFileUrl($pathInfo, null, null);
$this->renderImageBox($renderer, $imageUrl, $fullUrl, $data);
return true;
}
/**
* Build the file.php URL for the image.
*
* @param array $pathInfo Path info from Path helper
* @param int|null $width Optional width
* @param int|null $height Optional height
* @return string
*/
protected function buildFileUrl(array $pathInfo, ?int $width, ?int $height): string
{
global $ID;
$params = [
'root' => $pathInfo['root'],
'file' => $pathInfo['local'],
'id' => $ID,
];
if ($width !== null) {
$params['w'] = $width;
}
if ($height !== null) {
$params['h'] = $height;
}
return DOKU_BASE . 'lib/plugins/luxtools/file.php?' . http_build_query($params, '', '&');
}
/**
* Render the imagebox HTML.
*
* @param \Doku_Renderer $renderer
* @param string $imageUrl URL for the displayed image
* @param string $fullUrl URL for the full-size image (on click)
* @param array $data Parsed data from handle()
*/
protected function renderImageBox(\Doku_Renderer $renderer, string $imageUrl, 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;"';
}
// Build image attributes
$imgAttrs = 'class="media" 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 .= '<div class="luxtools-imagebox ' . $alignClass . '"' . $outerStyle . '>';
$renderer->doc .= '<div class="luxtools-imagebox-inner">';
// Image with link to full size
$renderer->doc .= '<a href="' . hsc($fullUrl) . '" class="media" target="_blank">';
$renderer->doc .= '<img src="' . hsc($imageUrl) . '" ' . $imgAttrs . ' />';
$renderer->doc .= '</a>';
// Caption
if ($caption !== '') {
// Calculate max caption width
$captionStyle = '';
if ($width !== null) {
$captionStyle = ' style="max-width: ' . ($width - 6) . 'px;"';
}
$renderer->doc .= '<div class="luxtools-imagebox-caption"' . $captionStyle . '>';
$renderer->doc .= hsc($caption);
$renderer->doc .= '</div>';
}
$renderer->doc .= '</div>';
$renderer->doc .= '</div>';
}
}

View File

@@ -42,13 +42,6 @@ class syntax_plugin_luxtools_scratchpad extends SyntaxPlugin
/** @inheritdoc */
public function handle($match, $state, $pos, Doku_Handler $handler)
{
global $INPUT;
// Do not allow the syntax in discussion plugin comments
if (!$this->getConf('allow_in_comments') && $INPUT->has('comment')) {
return false;
}
$match = substr($match, strlen('{{scratchpad>'), -2);
[$path,] = array_pad(explode('&', $match, 2), 2, '');