From 9a067eca1640c1cc26263f9f02fd6ade2f250a0e Mon Sep 17 00:00:00 2001 From: luxick Date: Mon, 5 Jan 2026 10:49:00 +0100 Subject: [PATCH] Inital implementation of image listing --- Output.php | 34 ++++++++ _test/SyntaxTest.php | 21 +++++ syntax.php | 136 ++----------------------------- syntax/files.php | 138 +++++++++++++++++++++++++++++++ syntax/images.php | 189 +++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 390 insertions(+), 128 deletions(-) create mode 100644 syntax/files.php create mode 100644 syntax/images.php diff --git a/Output.php b/Output.php index 0982114..e2d5086 100644 --- a/Output.php +++ b/Output.php @@ -38,6 +38,40 @@ class Output } } + /** + * Render a thumbnail gallery (XHTML only). + * + * Expects a flat list of file items in $this->files. + * Clicking a thumbnail opens the original image. + * + * @param array $params + * @return void + */ + public function renderAsGallery($params) + { + if (!($this->renderer instanceof \Doku_Renderer_xhtml)) { + $params['style'] = 'list'; + $this->renderAsList($params); + return; + } + + /** @var \Doku_Renderer_xhtml $renderer */ + $renderer = $this->renderer; + $renderer->doc .= ''; + } + /** * Renders the files as a table, including details if configured that way. * diff --git a/_test/SyntaxTest.php b/_test/SyntaxTest.php index 8a31125..7a1024a 100644 --- a/_test/SyntaxTest.php +++ b/_test/SyntaxTest.php @@ -192,4 +192,25 @@ class plugin_filetools_test extends DokuWikiTest $this->structureCheck($doc, $structure); } + + /** + * This function checks that the images syntax renders a thumbnail gallery. + */ + public function test_images_gallery() + { + $instructions = p_get_instructions('{{images>' . TMP_DIR . '/filelistdata/*&direct=1}}'); + $xhtml = p_render('xhtml', $instructions, $info); + + $doc = new Document(); + $doc->html($xhtml); + + $structure = [ + 'div.filetools-plugin.filetools-gallery' => 1, + 'div.filetools-plugin.filetools-gallery a' => 1, + 'div.filetools-plugin.filetools-gallery img' => 1, + ]; + $this->structureCheck($doc, $structure); + + $this->assertStringContainsString('exampleimage.png', $xhtml); + } } diff --git a/syntax.php b/syntax.php index b738b06..86b70f8 100644 --- a/syntax.php +++ b/syntax.php @@ -1,140 +1,20 @@ + * Keep this class so existing code that does `plugin_load('syntax', 'filetools')` + * continues to work (config/lang access). + * + * The actual {{files>...}} syntax implementation lives in syntax/files.php. */ -class syntax_plugin_filetools extends SyntaxPlugin +class syntax_plugin_filetools extends syntax_plugin_filetools_files { - /** @inheritdoc */ - public function getType() - { - return 'substition'; - } - - /** @inheritdoc */ - public function getPType() - { - return 'block'; - } - - /** @inheritdoc */ - public function getSort() - { - return 222; - } - /** @inheritdoc */ public function connectTo($mode) { - $this->Lexer->addSpecialPattern('\{\{files>.+?\}\}', $mode, 'plugin_filetools'); - } - - /** @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('{{files>'), -2); - [$path, $flags] = explode('&', $match, 2); - - // load default config options - $flags = $this->getConf('defaults') . '&' . $flags; - $flags = explode('&', $flags); - - $params = [ - 'sort' => 'name', - 'order' => 'asc', - 'style' => 'list', - 'tableheader' => 0, - 'recursive' => 0, - 'titlefile' => '_title.txt', - 'cache' => 0, - 'randlinks' => 0, - 'showsize' => 0, - 'showdate' => 0, - 'listsep' => ', ', - ]; - foreach ($flags as $flag) { - [$name, $value] = sexplode('=', $flag, 2, ''); - $params[trim($name)] = trim(trim($value), '"'); // quotes can be use to keep whitespace - } - - // separate path and pattern - $path = Path::cleanPath($path, false); - $parts = explode('/', $path); - $pattern = array_pop($parts); - $base = implode('/', $parts) . '/'; - - return [$base, $pattern, $params]; - } - - /** - * Create output - */ - public function render($format, Doku_Renderer $renderer, $data) - { - [$base, $pattern, $params] = $data; - - if ($format != 'xhtml' && $format != 'odt') { - return false; - } - - // disable caching - if ($params['cache'] === 0) { - $renderer->nocache(); - } - - - try { - $pathHelper = new Path($this->getConf('paths')); - $pathInfo = $pathHelper->getPathInfo($base); - } catch (Exception $e) { - $renderer->cdata('[n/a: ' . $this->getLang('error_outsidejail') . ']'); - return true; - } - - $crawler = new Crawler($this->getConf('extensions')); - $crawler->setSortBy($params['sort']); - $crawler->setSortReverse($params['order'] === 'desc'); - - $result = $crawler->crawl( - $pathInfo['root'], - $pathInfo['local'], - $pattern, - $params['recursive'], - $params['titlefile'] - ); - - // if we got nothing back, display a message - if ($result == []) { - $renderer->cdata('[n/a: ' . $this->getLang('error_nomatch') . ']'); - return true; - } - - $output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $result); - - switch ($params['style']) { - case 'list': - case 'olist': - $output->renderAsList($params); - break; - case 'table': - $output->renderAsTable($params); - break; - } - return true; + // Intentionally empty: syntax is registered by syntax_plugin_filetools_files. } } diff --git a/syntax/files.php b/syntax/files.php new file mode 100644 index 0000000..240dd6c --- /dev/null +++ b/syntax/files.php @@ -0,0 +1,138 @@ +Lexer->addSpecialPattern('\{\{files>.+?\}\}', $mode, 'plugin_filetools_files'); + } + + /** @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('{{files>'), -2); + [$path, $flags] = explode('&', $match, 2); + + // load default config options + $flags = $this->getConf('defaults') . '&' . $flags; + $flags = explode('&', $flags); + + $params = [ + 'sort' => 'name', + 'order' => 'asc', + 'style' => 'list', + 'tableheader' => 0, + 'recursive' => 0, + 'titlefile' => '_title.txt', + 'cache' => 0, + 'randlinks' => 0, + 'showsize' => 0, + 'showdate' => 0, + 'listsep' => ', ', + ]; + foreach ($flags as $flag) { + [$name, $value] = sexplode('=', $flag, 2, ''); + $params[trim($name)] = trim(trim($value), '"'); // quotes can be use to keep whitespace + } + + // separate path and pattern + $path = Path::cleanPath($path, false); + $parts = explode('/', $path); + $pattern = array_pop($parts); + $base = implode('/', $parts) . '/'; + + return [$base, $pattern, $params]; + } + + /** + * Create output + */ + public function render($format, Doku_Renderer $renderer, $data) + { + [$base, $pattern, $params] = $data; + + if ($format != 'xhtml' && $format != 'odt') { + return false; + } + + // disable caching + if ($params['cache'] === 0) { + $renderer->nocache(); + } + + try { + $pathHelper = new Path($this->getConf('paths')); + $pathInfo = $pathHelper->getPathInfo($base); + } catch (Exception $e) { + $renderer->cdata('[n/a: ' . $this->getLang('error_outsidejail') . ']'); + return true; + } + + $crawler = new Crawler($this->getConf('extensions')); + $crawler->setSortBy($params['sort']); + $crawler->setSortReverse($params['order'] === 'desc'); + + $result = $crawler->crawl( + $pathInfo['root'], + $pathInfo['local'], + $pattern, + $params['recursive'], + $params['titlefile'] + ); + + // if we got nothing back, display a message + if ($result == []) { + $renderer->cdata('[n/a: ' . $this->getLang('error_nomatch') . ']'); + return true; + } + + $output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $result); + + switch ($params['style']) { + case 'list': + case 'olist': + $output->renderAsList($params); + break; + case 'table': + $output->renderAsTable($params); + break; + } + return true; + } +} diff --git a/syntax/images.php b/syntax/images.php new file mode 100644 index 0000000..7d21fad --- /dev/null +++ b/syntax/images.php @@ -0,0 +1,189 @@ +Lexer->addSpecialPattern('\{\{images>.+?\}\}', $mode, 'plugin_filetools_images'); + } + + /** @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('{{images>'), -2); + [$path, $flags] = explode('&', $match, 2); + + // load default config options + $flags = $this->getConf('defaults') . '&' . $flags; + $flags = explode('&', $flags); + + $params = [ + 'sort' => 'name', + 'order' => 'asc', + 'recursive' => 0, + 'titlefile' => '_title.txt', + 'cache' => 0, + 'randlinks' => 0, + ]; + + foreach ($flags as $flag) { + [$name, $value] = sexplode('=', $flag, 2, ''); + $params[trim($name)] = trim(trim($value), '"'); // quotes can be use to keep whitespace + } + + // separate path and pattern + $path = Path::cleanPath($path, false); + $parts = explode('/', $path); + $pattern = array_pop($parts); + $base = implode('/', $parts) . '/'; + + return [$base, $pattern, $params]; + } + + /** + * Create output + */ + public function render($format, Doku_Renderer $renderer, $data) + { + [$base, $pattern, $params] = $data; + + if ($format != 'xhtml' && $format != 'odt') { + return false; + } + + // disable caching + if ($params['cache'] === 0) { + $renderer->nocache(); + } + + try { + $pathHelper = new Path($this->getConf('paths')); + $pathInfo = $pathHelper->getPathInfo($base); + } catch (Exception $e) { + $renderer->cdata('[n/a: ' . $this->getLang('error_outsidejail') . ']'); + return true; + } + + $crawler = new Crawler($this->getConf('extensions')); + $crawler->setSortBy($params['sort']); + $crawler->setSortReverse($params['order'] === 'desc'); + + $result = $crawler->crawl( + $pathInfo['root'], + $pathInfo['local'], + $pattern, + $params['recursive'], + $params['titlefile'] + ); + + $items = $this->flattenResultTree($result); + $items = $this->filterImages($items); + + // if we got nothing back, display a message + if ($items == []) { + $renderer->cdata('[n/a: ' . $this->getLang('error_nomatch') . ']'); + return true; + } + + $output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $items); + + if ($format == 'xhtml') { + $output->renderAsGallery($params); + return true; + } + + // Fallback for non-XHTML formats: render as a list of links + $params['style'] = 'list'; + $params['showsize'] = 0; + $params['showdate'] = 0; + $params['listsep'] = ', '; + $output->renderAsList($params); + return true; + } + + /** + * Flattens the crawl result tree into a list of file items. + * + * @param array $items + * @param string $prefix + * @return array + */ + protected function flattenResultTree($items, $prefix = '') + { + $result = []; + foreach ($items as $file) { + if ($file['children'] !== false) { + $result = array_merge( + $result, + $this->flattenResultTree($file['children'], $prefix . $file['name'] . '/') + ); + } else { + $file['name'] = $prefix . $file['name']; + $result[] = $file; + } + } + return $result; + } + + /** + * Keep only image files. + * + * @param array $items + * @return array + */ + protected function filterImages($items) + { + $images = []; + foreach ($items as $item) { + if (!isset($item['path']) || !is_string($item['path'])) continue; + if (!is_file($item['path'])) continue; + + try { + [, $mime,] = mimetype($item['path'], false); + } catch (Throwable $e) { + continue; + } + + if (is_string($mime) && str_starts_with($mime, 'image/')) { + $images[] = $item; + } + } + return $images; + } +}