diff --git a/README.md b/README.md index daafad0..1649907 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,25 @@ This always renders as a table. It includes an "Open Location" link above the ta Clicking a thumbnail opens a lightbox viewer. Thumbnails are generated and cached via the plugin endpoint. -### 4) Open a local path/folder (best-effort) +### 4) Single image with caption (imagebox) + +``` +{{image>/Scape/photos/picture.jpg|This is the caption}} +{{image>/Scape/photos/picture.jpg?400|Resized to 400px width}} +{{image>/Scape/photos/picture.jpg?400x300|Fixed dimensions}} +{{image>/Scape/photos/picture.jpg?left|Float left}} +{{image>/Scape/photos/picture.jpg?400¢er|Resized and centered}} +``` + +Renders a Wikipedia-style image box with optional caption. Parameters after `?` are separated by `&`: + +- Size: `?200` (width) or `?200x150` (width × height) +- Alignment: `?left`, `?right` (default), or `?center` +- Combined: `?400&left` or `?400x300¢er` + +The image links to the full-size version when clicked. + +### 5) Open a local path/folder (best-effort) ``` {{open>/Scape/projects|Open projects folder}} @@ -186,7 +204,7 @@ Behaviour: - Prefer calling the configured local client service (open_service_url). - Fall back to opening a file:// URL in a new tab (often blocked by browsers). -### 5) Scratchpads (shared, file-backed, no page revisions) +### 6) Scratchpads (shared, file-backed, no page revisions) ``` {{scratchpad>start}} @@ -221,6 +239,17 @@ Additionally for `{{files>...}}`: | tableheader | 0\|1 | Render table header row. | +## Admin settings + +The admin settings page includes a **default_tablecolumns** option that lets you specify which columns are displayed by default in table-style listings. This is a comma-separated list of column names: + +- `name` – File/folder name (always shown) +- `size` – File size +- `date` – Last modified date + +Example: `name,size,date` shows all columns by default. + + ## Credits / upstream luxtools is a fork of the [DokuWiki Filelist plugin](https://www.dokuwiki.org/plugin:filelist). diff --git a/admin/main.php b/admin/main.php index cf1ef2c..baeca73 100644 --- a/admin/main.php +++ b/admin/main.php @@ -12,7 +12,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin protected $configKeys = [ 'paths', 'scratchpad_paths', - 'allow_in_comments', 'extensions', 'default_sort', 'default_order', @@ -25,6 +24,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin 'default_randlinks', 'default_showsize', 'default_showdate', + 'default_tablecolumns', 'default_listsep', 'default_maxheight', 'thumb_placeholder', @@ -69,7 +69,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin $scratchpadPaths = str_replace(["\r\n", "\r"], "\n", $scratchpadPaths); $newConf['scratchpad_paths'] = $scratchpadPaths; - $newConf['allow_in_comments'] = (int)$INPUT->bool('allow_in_comments'); $newConf['extensions'] = $INPUT->str('extensions'); $newConf['default_sort'] = $INPUT->str('default_sort'); @@ -83,6 +82,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin $newConf['default_randlinks'] = (int)$INPUT->bool('default_randlinks'); $newConf['default_showsize'] = (int)$INPUT->bool('default_showsize'); $newConf['default_showdate'] = (int)$INPUT->bool('default_showdate'); + $newConf['default_tablecolumns'] = $INPUT->str('default_tablecolumns'); $newConf['default_listsep'] = $INPUT->str('default_listsep'); $newConf['default_maxheight'] = $INPUT->str('default_maxheight'); @@ -126,12 +126,6 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin echo '' . hsc($scratchpadPaths) . ''; echo ''; - // allow_in_comments - $checked = $this->getConf('allow_in_comments') ? ' checked="checked"' : ''; - echo '' . hsc($this->getLang('allow_in_comments')) . ' '; - echo ''; - echo ''; - // extensions echo '' . hsc($this->getLang('extensions')) . ' '; echo ''; @@ -219,6 +213,11 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin echo ''; echo ''; + // default_tablecolumns + echo '' . hsc($this->getLang('default_tablecolumns')) . ''; + echo ''; + echo ''; + // default_listsep echo '' . hsc($this->getLang('default_listsep')) . ''; echo ''; diff --git a/conf/default.php b/conf/default.php index 1d6f79e..378f166 100644 --- a/conf/default.php +++ b/conf/default.php @@ -6,7 +6,6 @@ $conf['paths'] = ''; $conf['scratchpad_paths'] = ''; -$conf['allow_in_comments'] = 0; // Legacy (advanced): additional default flags in the same syntax as inline options. $conf['defaults'] = ''; $conf['extensions'] = ''; @@ -23,6 +22,7 @@ $conf['default_cache'] = 0; // 0|1 $conf['default_randlinks'] = 0; // 0|1 $conf['default_showsize'] = 0; // 0|1 $conf['default_showdate'] = 0; // 0|1 +$conf['default_tablecolumns'] = 'name'; // Comma-separated: name, size, date $conf['default_listsep'] = ', '; $conf['default_maxheight'] = 500; // -1 disables scroll container diff --git a/lang/de/lang.php b/lang/de/lang.php index 7babb7e..0078d12 100644 --- a/lang/de/lang.php +++ b/lang/de/lang.php @@ -28,7 +28,6 @@ $lang['err_security'] = 'Sicherheits-Token ungültig. Bitte erneut versuchen.'; $lang['paths'] = 'Erlaubte Basis-Pfade (eine pro Zeile). Optional: Pfad mit A>-Alias ergaenzen.'; $lang['scratchpad_paths'] = 'Scratchpad-Dateien (eine pro Zeile). Jeder Dateipfad muss eine Erweiterung enthalten. Mit einer folgenden A>-Zeile wird der Pad-Name gesetzt, der im Wiki verwendet wird.'; -$lang['allow_in_comments'] = 'luxtools-Syntax (files/images/directory/scratchpad) in Kommentaren erlauben.'; $lang['extensions'] = 'Kommagetrennte Liste erlaubter Dateiendungen.'; $lang['listing_defaults'] = 'Listen-Standardwerte'; diff --git a/lang/de/settings.php b/lang/de/settings.php index aa43fe4..0eb9e73 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -2,7 +2,6 @@ $lang["paths"] = "Erlaubte Basis-Pfade (eine pro Zeile). Optional: Pfad mit A>-Alias ergaenzen."; $lang["scratchpad_paths"] = "Scratchpad-Dateien (eine pro Zeile). Jeder Dateipfad muss eine Erweiterung enthalten. Mit einer folgenden A>-Zeile wird der Pad-Name gesetzt, der im Wiki verwendet wird."; -$lang["allow_in_comments"] = "luxtools-Syntax (files/images/directory/scratchpad) in Kommentaren erlauben."; $lang["extensions"] = "Kommagetrennte Liste erlaubter Dateiendungen."; $lang["listing_defaults"] = "Listen-Standardwerte"; diff --git a/lang/en/lang.php b/lang/en/lang.php index fd42330..dfc8fbd 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -28,7 +28,6 @@ $lang['err_security'] = 'Security token mismatch. Please retry.'; $lang['paths'] = 'Allowed base paths (one per line). Optional: follow a path with A> alias.'; $lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension. Use a following A> line to set the pad name used in the wiki.'; -$lang['allow_in_comments'] = 'Whether to allow luxtools syntax (files/images/directory/scratchpad) to be used in comments.'; $lang['extensions'] = 'Comma-separated list of allowed file extensions to list.'; $lang['listing_defaults'] = 'Listing defaults'; @@ -43,6 +42,7 @@ $lang['default_cache'] = 'Enable page caching by default (0 disables caching).'; $lang['default_randlinks'] = 'Add cache-busting query parameter based on mtime by default.'; $lang['default_showsize'] = 'Show file size by default (where supported).'; $lang['default_showdate'] = 'Show last modified date by default (where supported).'; +$lang['default_tablecolumns'] = 'Default table columns (comma-separated). Available: name, size, date. Example: "name,size,date" shows all columns.'; $lang['default_listsep'] = 'Default separator used in list-style rendering (e.g. ", ").'; $lang['default_maxheight'] = 'Default max-height in px for scroll container (-1 disables).'; diff --git a/lang/en/settings.php b/lang/en/settings.php index 2e0e347..a3c7507 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -2,7 +2,6 @@ $lang['paths'] = 'Allowed base paths (one per line). Optional: follow a path with A> alias.'; $lang['scratchpad_paths'] = 'Scratchpad files (one per line). Each file path must include the extension; use a following A> line to set the pad name used in the wiki.'; -$lang['allow_in_comments'] = 'Whether to allow luxtools syntax (files/images/directory/scratchpad) to be used in comments.'; $lang['extensions'] = 'Comma-separated list of allowed file extensions to list'; $lang['listing_defaults'] = 'Listing defaults'; diff --git a/src/Output.php b/src/Output.php index 8b339f7..6136382 100644 --- a/src/Output.php +++ b/src/Output.php @@ -341,29 +341,43 @@ class Output } $renderer->tabletbody_open(); - foreach ($items as $item) { + + if ($items === []) { + // Render a single row with an empty state message. $renderer->tablerow_open(); - $renderer->tablecell_open(); - $this->renderItemLink($item, $params['randlinks']); + $renderer->tablecell_open($columns); + if ($renderer instanceof \Doku_Renderer_xhtml) { + $renderer->doc .= '' . hsc($this->getLang('empty_files')) . ''; + } else { + $renderer->cdata($this->getLang('empty_files')); + } $renderer->tablecell_close(); - - if ($params['showsize']) { - $renderer->tablecell_open(1, 'right'); - if (!empty($item['isdir'])) { - $renderer->cdata(''); - } else { - $renderer->cdata(filesize_h($item['size'])); - } - $renderer->tablecell_close(); - } - - if ($params['showdate']) { - $renderer->tablecell_open(); - $renderer->cdata(dformat($item['mtime'])); - $renderer->tablecell_close(); - } - $renderer->tablerow_close(); + } else { + foreach ($items as $item) { + $renderer->tablerow_open(); + $renderer->tablecell_open(); + $this->renderItemLink($item, $params['randlinks']); + $renderer->tablecell_close(); + + if ($params['showsize']) { + $renderer->tablecell_open(1, 'right'); + if (!empty($item['isdir'])) { + $renderer->cdata(''); + } else { + $renderer->cdata(filesize_h($item['size'])); + } + $renderer->tablecell_close(); + } + + if ($params['showdate']) { + $renderer->tablecell_open(); + $renderer->cdata(dformat($item['mtime'])); + $renderer->tablecell_close(); + } + + $renderer->tablerow_close(); + } } $renderer->tabletbody_close(); $renderer->table_close(); diff --git a/style.css b/style.css index e3f8d7b..d4eccd1 100644 --- a/style.css +++ b/style.css @@ -392,3 +392,65 @@ html.luxtools-noscroll body { font-size: 2.4em; } } + +/* ======================================================================== + * Imagebox (Wikipedia-style image with caption) + * ======================================================================== */ + +.luxtools-imagebox { + margin-bottom: 0.5em; + display: block; +} + +.luxtools-imagebox.tleft { + clear: left; + float: left; + margin-right: 1.4em; +} + +.luxtools-imagebox.tright { + clear: right; + float: right; + margin-left: 1.4em; +} + +.luxtools-imagebox.tcenter { + clear: both; + text-align: center; + margin-left: auto; + margin-right: auto; +} + +.luxtools-imagebox.tcenter .luxtools-imagebox-inner { + display: inline-block; + text-align: left; +} + +.luxtools-imagebox .luxtools-imagebox-inner { + display: inline-block; + background-color: @ini_background_alt; + border: 1px solid @ini_border; + padding: 3px; + font-size: 94%; + overflow: hidden; +} + +.luxtools-imagebox .luxtools-imagebox-inner > a { + display: block; + line-height: 0; + background-color: @ini_background; +} + +.luxtools-imagebox .luxtools-imagebox-inner img { + display: block; + max-width: 100%; + height: auto; +} + +.luxtools-imagebox .luxtools-imagebox-caption { + border: none; + font-size: 94%; + line-height: 1.4em; + padding: 3px; + text-align: left; +} diff --git a/syntax/AbstractSyntax.php b/syntax/AbstractSyntax.php index 11a9b9a..f3f0cbb 100644 --- a/syntax/AbstractSyntax.php +++ b/syntax/AbstractSyntax.php @@ -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'), ]; diff --git a/syntax/directory.php b/syntax/directory.php index 5b7b7f6..2ffbfc9 100644 --- a/syntax/directory.php +++ b/syntax/directory.php @@ -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; diff --git a/syntax/files.php b/syntax/files.php index c28b4ba..9c4cb37 100644 --- a/syntax/files.php +++ b/syntax/files.php @@ -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); diff --git a/syntax/image.php b/syntax/image.php new file mode 100644 index 0000000..b94f36d --- /dev/null +++ b/syntax/image.php @@ -0,0 +1,242 @@ +/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¢er - 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 .= ''; + $renderer->doc .= ''; + + // Image with link to full size + $renderer->doc .= ''; + $renderer->doc .= ''; + $renderer->doc .= ''; + + // Caption + if ($caption !== '') { + // Calculate max caption width + $captionStyle = ''; + if ($width !== null) { + $captionStyle = ' style="max-width: ' . ($width - 6) . 'px;"'; + } + $renderer->doc .= ''; + $renderer->doc .= hsc($caption); + $renderer->doc .= ''; + } + + $renderer->doc .= ''; + $renderer->doc .= ''; + } +} diff --git a/syntax/scratchpad.php b/syntax/scratchpad.php index 0c6b41d..bd44861 100644 --- a/syntax/scratchpad.php +++ b/syntax/scratchpad.php @@ -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, '');