diff --git a/Crawler.php b/Crawler.php
index aaecc9b..9216dac 100644
--- a/Crawler.php
+++ b/Crawler.php
@@ -122,6 +122,77 @@ class Crawler
return $this->sortItems($result);
}
+ /**
+ * List the direct children (files and directories) of a given local path.
+ *
+ * Unlike crawl(), this includes directories even when not recursing.
+ *
+ * @param string $root
+ * @param string $local
+ * @param string $titlefile
+ * @return array
+ */
+ public function listDirectory($root, $local, $titlefile)
+ {
+ $path = $root . $local;
+ $path = rtrim($path, '/');
+
+ // do not list wiki or data directories
+ if (Path::isWikiControlled($path)) return [];
+
+ if (($dir = opendir($path)) === false) return [];
+ $result = [];
+ while (($file = readdir($dir)) !== false) {
+ if ($file[0] == '.' || $file == $titlefile) {
+ // ignore hidden, system and title files
+ continue;
+ }
+
+ $filepath = $path . '/' . $file;
+ if (!is_readable($filepath)) continue;
+
+ $isDir = is_dir($filepath);
+ if (!$isDir && !$this->isExtensionAllowed($file)) {
+ continue;
+ }
+ if ($this->isFileIgnored($file)) {
+ continue;
+ }
+
+ // get title file (directories only)
+ $filename = $file;
+ if ($isDir) {
+ $title = $filepath . '/' . $titlefile;
+ if (is_readable($title)) {
+ $filename = io_readFile($title, false);
+ }
+ }
+
+ // build a local path consistent with crawl() (leading slash for root)
+ $self = rtrim($local, '/') . '/' . $file;
+ if ($self === '/' . $file) {
+ // keep the original behaviour when local is empty
+ $self = '/' . $file;
+ }
+
+ $entry = [
+ 'name' => $filename,
+ 'local' => $self,
+ 'path' => $filepath,
+ 'mtime' => filemtime($filepath),
+ 'ctime' => filectime($filepath),
+ 'size' => $isDir ? 0 : filesize($filepath),
+ 'children' => false,
+ 'treesize' => 1,
+ 'isdir' => $isDir,
+ ];
+
+ $result[] = $entry;
+ }
+ closedir($dir);
+ return $this->sortItems($result);
+ }
+
/**
* Sort the given items by the current sortby and sortreverse settings
*
diff --git a/Output.php b/Output.php
index d9c9678..4545822 100644
--- a/Output.php
+++ b/Output.php
@@ -91,6 +91,25 @@ class Output
}
}
+ /**
+ * Render a flat list (files and/or directories) as a table.
+ *
+ * @param array $params
+ * @return void
+ */
+ public function renderAsFlatTable($params)
+ {
+ if ($this->renderer instanceof \Doku_Renderer_xhtml) {
+ $this->renderer->doc .= '
';
+ }
+
+ $this->renderTableItems($this->files, $params);
+
+ if ($this->renderer instanceof \Doku_Renderer_xhtml) {
+ $this->renderer->doc .= '
';
+ }
+ }
+
/**
* Renders the files as a table, including details if configured that way.
@@ -147,7 +166,11 @@ class Output
if ($params['showsize']) {
$renderer->tablecell_open(1, 'right');
- $renderer->cdata(filesize_h($item['size']));
+ if (!empty($item['isdir'])) {
+ $renderer->cdata('');
+ } else {
+ $renderer->cdata(filesize_h($item['size']));
+ }
$renderer->tablecell_close();
}
@@ -217,6 +240,11 @@ class Output
protected function renderItemLink($item, $cachebuster = false)
{
+ if (!empty($item['isdir'])) {
+ $this->renderDirectoryLink($item);
+ return;
+ }
+
if ($this->renderer instanceof \Doku_Renderer_xhtml) {
$this->renderItemLinkXHTML($item, $cachebuster);
} else {
@@ -224,6 +252,60 @@ class Output
}
}
+ /**
+ * Render a directory like a normal media link, but with open behaviour.
+ *
+ * @param array $item
+ * @return void
+ */
+ protected function renderDirectoryLink($item)
+ {
+ $caption = $item['name'] ?? '';
+ $path = $item['path'] ?? '';
+
+ if ($caption === '') {
+ $caption = '[n/a]';
+ }
+
+ if (!($this->renderer instanceof \Doku_Renderer_xhtml)) {
+ $this->renderer->cdata($caption);
+ return;
+ }
+
+ if (!is_string($path) || $path === '') {
+ $this->renderer->cdata('[n/a]');
+ return;
+ }
+
+ global $conf;
+ /** @var \Doku_Renderer_xhtml $renderer */
+ $renderer = $this->renderer;
+
+ $syntax = plugin_load('syntax', 'luxtools');
+ $serviceUrl = $syntax ? trim((string)$syntax->getConf('open_service_url')) : '';
+ $serviceToken = $syntax ? trim((string)$syntax->getConf('open_service_token')) : '';
+
+ // Prepare a DokuWiki-style media link with a folder icon class.
+ $link = [
+ 'target' => $conf['target']['extern'],
+ 'style' => '',
+ 'pre' => '',
+ 'suf' => '',
+ 'name' => $caption,
+ 'url' => '#',
+ 'title' => $renderer->_xmlEntities($path),
+ 'more' => ' class="filetools-open media mediafile mf_folder" data-path="' . hsc($path) . '"',
+ ];
+ if ($conf['relnofollow']) $link['more'] .= ' rel="nofollow"';
+ if ($serviceUrl !== '') {
+ $link['more'] .= ' data-service-url="' . hsc($serviceUrl) . '"';
+ }
+ if ($serviceToken !== '') {
+ $link['more'] .= ' data-service-token="' . hsc($serviceToken) . '"';
+ }
+ $renderer->doc .= $renderer->_formatLink($link);
+ }
+
/**
* Render a file link on the XHTML renderer
*/
diff --git a/_test/SyntaxTest.php b/_test/SyntaxTest.php
index f694014..b5893c0 100644
--- a/_test/SyntaxTest.php
+++ b/_test/SyntaxTest.php
@@ -233,4 +233,34 @@ class plugin_luxtools_test extends DokuWikiTest
$this->assertStringContainsString('Open here', $xhtml);
$this->assertStringContainsString('data-path="/tmp/somewhere"', $xhtml);
}
+
+ /**
+ * This function checks that the directory syntax renders a flat table,
+ * listing both folders and files.
+ */
+ public function test_directory_table_flat()
+ {
+ $instructions = p_get_instructions('{{directory>' . TMP_DIR . '/filelistdata/&direct=1}}');
+ $xhtml = p_render('xhtml', $instructions, $info);
+
+ $doc = new Document();
+ $doc->html($xhtml);
+
+ $structure = [
+ 'div.filetools-plugin' => 1,
+ 'div.filetools-plugin table' => 1,
+ 'div.filetools-plugin table > tbody > tr' => 3,
+ 'a.filetools-open' => 1,
+ ];
+ $this->structureCheck($doc, $structure);
+
+ // Should list the top-level entries, but not recurse into exampledir
+ $this->assertStringContainsString('example.txt', $xhtml);
+ $this->assertStringContainsString('exampleimage.png', $xhtml);
+ $this->assertStringContainsString('exampledir', $xhtml);
+ $this->assertStringNotContainsString('example2.txt', $xhtml);
+
+ // Directory row should trigger the same behaviour as {{open>...}} for that folder
+ $this->assertStringContainsString('data-path="' . TMP_DIR . '/filelistdata/exampledir"', $xhtml);
+ }
}
diff --git a/syntax/directory.php b/syntax/directory.php
new file mode 100644
index 0000000..84a568b
--- /dev/null
+++ b/syntax/directory.php
@@ -0,0 +1,127 @@
+Lexer->addSpecialPattern('\{\{directory>.+?\}\}', $mode, 'plugin_luxtools_directory');
+ }
+
+ /** @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('{{directory>'), -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 used to keep whitespace
+ }
+
+ // directory path (no glob/pattern)
+ $path = Path::cleanPath($path, true);
+
+ return [$path, $params];
+ }
+
+ /** @inheritdoc */
+ public function render($format, Doku_Renderer $renderer, $data)
+ {
+ if ($data === false) return false;
+ [$path, $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($path);
+ } 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');
+
+ $items = $crawler->listDirectory(
+ $pathInfo['root'],
+ $pathInfo['local'],
+ $params['titlefile']
+ );
+
+ if ($items == []) {
+ $renderer->cdata('[n/a: ' . $this->getLang('error_nomatch') . ']');
+ return true;
+ }
+
+ // Always render as table style
+ $params['style'] = 'table';
+
+ $output = new Output($renderer, $pathInfo['root'], $pathInfo['web'], $items);
+ $output->renderAsFlatTable($params);
+ return true;
+ }
+}