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;
+ }
+}