diff --git a/Crawler.php b/Crawler.php
index 9216dac..b0cb486 100644
--- a/Crawler.php
+++ b/Crawler.php
@@ -13,6 +13,9 @@ class Crawler
/** @var bool */
protected $sortreverse = false;
+ /** @var bool */
+ protected $foldersFirst = false;
+
/** @var string[] patterns to ignore */
protected $ignore = [];
@@ -41,6 +44,11 @@ class Crawler
$this->sortreverse = $sortreverse;
}
+ public function setFoldersFirst($foldersFirst)
+ {
+ $this->foldersFirst = (bool)$foldersFirst;
+ }
+
/**
* Does a (recursive) crawl for finding files based on a given pattern.
* Based on a safe glob reimplementation using fnmatch and opendir.
@@ -204,13 +212,41 @@ class Crawler
$callback = [$this, 'compare' . ucfirst($this->sortby)];
if (!is_callable($callback)) return $items;
- usort($items, $callback);
- if ($this->sortreverse) {
- $items = array_reverse($items);
+ // Optional grouping: keep directories before files.
+ // Implement reverse ordering by inverting comparisons instead of array_reverse(),
+ // so the directory-first grouping stays intact.
+ if ($this->foldersFirst) {
+ usort($items, function ($a, $b) use ($callback) {
+ $aIsDir = $this->isDirectoryItem($a);
+ $bIsDir = $this->isDirectoryItem($b);
+ if ($aIsDir !== $bIsDir) {
+ return $aIsDir ? -1 : 1;
+ }
+
+ $cmp = call_user_func($callback, $a, $b);
+ if ($this->sortreverse) $cmp = -$cmp;
+ return $cmp;
+ });
+ } else {
+ usort($items, $callback);
+ if ($this->sortreverse) {
+ $items = array_reverse($items);
+ }
}
return $items;
}
+ /**
+ * Detect whether an item represents a directory.
+ * Supports both crawl() results (children tree) and listDirectory() results (isdir).
+ */
+ protected function isDirectoryItem($item)
+ {
+ if (!is_array($item)) return false;
+ if (!empty($item['isdir'])) return true;
+ return array_key_exists('children', $item) && $item['children'] !== false;
+ }
+
/**
* Check if a file is allowed by the configured extensions
*
diff --git a/Output.php b/Output.php
index d84c9fd..d0d4fc8 100644
--- a/Output.php
+++ b/Output.php
@@ -265,27 +265,47 @@ class Output
$renderer->table_open($columns);
- if ($params['tableheader']) {
+ $hasOpenLocation = isset($params['openlocation']) && is_string($params['openlocation']) && trim($params['openlocation']) !== '';
+ $hasHeader = !empty($params['tableheader']);
+ if ($hasOpenLocation || $hasHeader) {
$renderer->tablethead_open();
- $renderer->tablerow_open();
- $renderer->tableheader_open();
- $renderer->cdata($this->getLang('filename'));
- $renderer->tableheader_close();
+ // Small row above the header with an "Open Location" link.
+ if ($hasOpenLocation && ($renderer instanceof \Doku_Renderer_xhtml)) {
+ $openItem = [
+ 'name' => $this->getLang('openlocation'),
+ 'path' => $params['openlocation'],
+ 'isdir' => true,
+ ];
- if ($params['showsize']) {
- $renderer->tableheader_open();
- $renderer->cdata($this->getLang('filesize'));
- $renderer->tableheader_close();
+ /** @var \Doku_Renderer_xhtml $renderer */
+ $renderer->doc .= '
| ';
+ $this->renderDirectoryLink($openItem);
+ $renderer->doc .= ' |
';
}
- if ($params['showdate']) {
+ if ($hasHeader) {
+ $renderer->tablerow_open();
+
$renderer->tableheader_open();
- $renderer->cdata($this->getLang('lastmodified'));
+ $renderer->cdata($this->getLang('filename'));
$renderer->tableheader_close();
+
+ if ($params['showsize']) {
+ $renderer->tableheader_open();
+ $renderer->cdata($this->getLang('filesize'));
+ $renderer->tableheader_close();
+ }
+
+ if ($params['showdate']) {
+ $renderer->tableheader_open();
+ $renderer->cdata($this->getLang('lastmodified'));
+ $renderer->tableheader_close();
+ }
+
+ $renderer->tablerow_close();
}
- $renderer->tablerow_close();
$renderer->tablethead_close();
}
diff --git a/lang/de/lang.php b/lang/de/lang.php
index 219d5ba..b9d2fd4 100644
--- a/lang/de/lang.php
+++ b/lang/de/lang.php
@@ -10,5 +10,6 @@
$lang['filename'] = 'Dateiname';
$lang['filesize'] = 'Dateigröße';
$lang['lastmodified'] = 'Letzte Änderung';
+$lang['openlocation'] = 'Ort öffnen';
$lang['error_nomatch'] = 'Keine Treffer';
$lang['error_outsidejail'] = 'Zugriff verweigert';
diff --git a/lang/en/lang.php b/lang/en/lang.php
index 982fd2a..918b671 100644
--- a/lang/en/lang.php
+++ b/lang/en/lang.php
@@ -10,5 +10,6 @@
$lang['filename'] = 'Filename';
$lang['filesize'] = 'Filesize';
$lang['lastmodified'] = 'Last modified';
+$lang['openlocation'] = 'Open Location';
$lang['error_nomatch'] = 'No match';
$lang['error_outsidejail'] = 'Access denied';
diff --git a/lang/nl/lang.php b/lang/nl/lang.php
index dd4ddaa..6d85d70 100644
--- a/lang/nl/lang.php
+++ b/lang/nl/lang.php
@@ -9,5 +9,6 @@
$lang['filename'] = 'Bestandsnaam';
$lang['filesize'] = 'Bestandsgrootte';
$lang['lastmodified'] = 'Laatst gewijzigd';
+$lang['openlocation'] = 'Locatie openen';
$lang['error_nomatch'] = 'Niets gevonden';
$lang['error_outsidejail'] = 'Toegang geweigerd';
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..65f0148
--- /dev/null
+++ b/style.css
@@ -0,0 +1,19 @@
+/* LuxTools plugin styles
+ * Keep this minimal and scoped to the plugin container.
+ */
+
+/* DokuWiki often highlights rows on hover. Avoid highlighting header rows. */
+div.filetools-plugin table thead tr:hover > * {
+ background-color: @ini_background_alt !important;
+}
+
+
+/* "Open Location" row above the header should be visually smaller. */
+div.filetools-plugin table thead tr.luxtools-openlocation-row td {
+ font-size: 80%;
+ padding-top: 0.2em;
+ padding-bottom: 0.2em;
+}
+div.filetools-plugin table thead tr.luxtools-openlocation-row:hover td {
+ background-color: @ini_background !important;
+}
diff --git a/syntax/AbstractSyntax.php b/syntax/AbstractSyntax.php
index b8fcfca..2d1e0c6 100644
--- a/syntax/AbstractSyntax.php
+++ b/syntax/AbstractSyntax.php
@@ -165,6 +165,7 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
'order' => 'asc',
'style' => 'list',
'tableheader' => 0,
+ 'foldersfirst' => 0,
'recursive' => 0,
'titlefile' => '_title.txt',
'cache' => 0,
@@ -222,6 +223,7 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin
$crawler = new Crawler($this->getConf('extensions'));
$crawler->setSortBy($params['sort']);
$crawler->setSortReverse($params['order'] === 'desc');
+ $crawler->setFoldersFirst(($params['foldersfirst'] ?? 0) != 0);
return $crawler;
}
diff --git a/syntax/directory.php b/syntax/directory.php
index 2b8a9ec..a448b7f 100644
--- a/syntax/directory.php
+++ b/syntax/directory.php
@@ -13,6 +13,15 @@ require_once(__DIR__ . '/AbstractSyntax.php');
*/
class syntax_plugin_luxtools_directory extends syntax_plugin_luxtools_abstract
{
+ /** @inheritdoc */
+ protected function getDefaultParams(): array
+ {
+ return [
+ // Directory listings should group folders before files by default.
+ 'foldersfirst' => 1,
+ ];
+ }
+
/** @inheritdoc */
protected function getSyntaxKeyword(): string
{
@@ -35,6 +44,9 @@ class syntax_plugin_luxtools_directory extends syntax_plugin_luxtools_abstract
return true;
}
+ // Provide the current directory path so Output can render the "Open Location" link.
+ $params['openlocation'] = $pathInfo['root'] . $pathInfo['local'];
+
$crawler = $this->createCrawler($params);
$items = $crawler->listDirectory(
$pathInfo['root'],