diff --git a/README.md b/README.md index 6845193..0cbade0 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ luxtools provides DokuWiki syntax that: - Renders an image thumbnail gallery (with lightbox) - Provides "open this folder/path" links for local workflows - Embeds file-backed scratchpads with a minimal inline editor (no wiki revisions) +- Links a page to a media folder via a UUID (.pagelink), enabling a `blobs/` alias It also ships a small file-serving endpoint (`lib/plugins/luxtools/file.php`) used to deliver files and generate cached thumbnails. @@ -141,6 +142,10 @@ Key settings: URL of a local client service used by `{{open>...}}` and directory links. See luxtools-client. +- **pagelink_search_depth** + Maximum directory depth for `.pagelink` discovery under each configured root. + `0` means only the root directory itself is checked. + ### Template style settings The `{{open>...}}` links and directory “open” links use a dedicated color @@ -206,6 +211,27 @@ Supported input examples include: - `2026-01-30 13:45` - `2026-01-30T13:45:00` +### 0.2) Editor toolbar: Page Link + +The **Page Link** toolbar button creates a page-scoped UUID and stores it in +page metadata. This UUID is used to link the page to a folder that contains +a `.pagelink` file with the same UUID. + +Workflow: + +1. Click **Page Link** in the editor toolbar to create the UUID. +2. View the page and copy the UUID from the “Not linked: Copy ID” status. +3. Create a `.pagelink` file in the target folder (within your configured + `paths` roots) and paste the UUID into that file. + +Once linked, you can use `blobs/` as an alias in luxtools syntax on that page, +for example: + +``` +{{images>blobs/*.png}} +{{directory>blobs/&recursive=1}} +``` + ### 1) List files by glob pattern The `{{directory>...}}` syntax (or `{{files>...}}` for backwards compatibility) can handle both directory listings and glob patterns. When a glob pattern is used, it renders as a table: diff --git a/action.php b/action.php index 804ca51..17b85b5 100644 --- a/action.php +++ b/action.php @@ -3,6 +3,9 @@ use dokuwiki\Extension\ActionPlugin; use dokuwiki\Extension\Event; use dokuwiki\Extension\EventHandler; +use dokuwiki\plugin\luxtools\PageLink; + +require_once(__DIR__ . '/autoload.php'); /** * luxtools action plugin: register JS assets. @@ -18,6 +21,12 @@ class action_plugin_luxtools extends ActionPlugin $this, "addScripts", ); + $controller->register_hook( + "TPL_CONTENT_DISPLAY", + "BEFORE", + $this, + "addPageLinkStatus", + ); $controller->register_hook( "CSS_STYLES_INCLUDED", "BEFORE", @@ -49,6 +58,7 @@ class action_plugin_luxtools extends ActionPlugin "open-service.js", "scratchpads.js", "date-fix.js", + "page-link.js", "linkfavicon.js", "main.js", ]; @@ -118,5 +128,80 @@ class action_plugin_luxtools extends ActionPlugin "icon" => "../../plugins/luxtools/images/date-fix-all.svg", "block" => false, ]; + + // Page Link: create a page-scoped UUID for .pagelink linking + $event->data[] = [ + "type" => "LuxtoolsPageLink", + "title" => $this->getLang("toolbar_pagelink_title"), + "icon" => "../../plugins/luxtools/images/pagelink.svg", + "block" => false, + ]; + } + + /** + * Inject page link status above the page content. + * + * @param Event $event + * @param mixed $param + * @return void + */ + public function addPageLinkStatus(Event $event, $param) + { + global $ACT, $ID; + + if (!is_string($ACT) || $ACT !== 'show') { + return; + } + + if (!is_string($ID) || $ID === '') { + return; + } + + $pageId = $ID; + if (function_exists('cleanID')) { + $pageId = (string)cleanID($pageId); + } + if ($pageId === '') return; + + $pathConfig = (string)$this->getConf('paths'); + $depth = (int)$this->getConf('pagelink_search_depth'); + if ($depth < 0) $depth = 0; + + $pageLink = new PageLink($pathConfig, $depth); + $uuid = $pageLink->getPageUuid($pageId); + if ($uuid === null) return; + + $linkInfo = $pageLink->resolveUuid($uuid); + $folder = $linkInfo['folder'] ?? null; + $multiple = !empty($linkInfo['multiple']); + + $statusText = ''; + $copyable = false; + $title = ''; + if (is_string($folder) && $folder !== '') { + $trimmed = rtrim($folder, '/\\'); + $statusText = basename($trimmed); + $title = $trimmed; + } else { + $statusText = (string)$this->getLang('pagelink_unlinked'); + $copyable = true; + } + + $warning = ''; + if ($multiple) { + $warning = ''; + } + + $html = '' + . hsc($statusText) + . $warning + . ''; + + $event->data = $html . $event->data; } } diff --git a/admin/main.php b/admin/main.php index 000b741..c1ae652 100644 --- a/admin/main.php +++ b/admin/main.php @@ -28,6 +28,7 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin 'thumb_placeholder', 'gallery_thumb_scale', 'open_service_url', + 'pagelink_search_depth', ]; public function getMenuText($language) @@ -86,6 +87,10 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin $newConf['gallery_thumb_scale'] = $INPUT->str('gallery_thumb_scale'); $newConf['open_service_url'] = $INPUT->str('open_service_url'); + $depth = (int)$INPUT->int('pagelink_search_depth'); + if ($depth < 0) $depth = 0; + $newConf['pagelink_search_depth'] = $depth; + if ($this->savePluginLocalConf($newConf)) { msg($this->getLang('saved'), 1); } else { @@ -223,6 +228,11 @@ class admin_plugin_luxtools_main extends DokuWiki_Admin_Plugin echo ''; echo '
'; + // pagelink_search_depth + echo '
'; + echo ''; echo ''; diff --git a/conf/default.php b/conf/default.php index a91607a..e1e1812 100644 --- a/conf/default.php +++ b/conf/default.php @@ -34,6 +34,9 @@ $conf['gallery_thumb_scale'] = 1; // Local client service used by {{open>...}}. $conf['open_service_url'] = 'http://127.0.0.1:8765'; +// Maximum depth when searching for .pagelink files under allowed roots. +$conf['pagelink_search_depth'] = 3; + // Image syntax defaults $conf['default_image_width'] = 250; $conf['default_image_align'] = 'right'; // left|right|center diff --git a/images/pagelink.svg b/images/pagelink.svg new file mode 100644 index 0000000..c5b2918 --- /dev/null +++ b/images/pagelink.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/js/page-link.js b/js/page-link.js new file mode 100644 index 0000000..e4023a6 --- /dev/null +++ b/js/page-link.js @@ -0,0 +1,136 @@ +/* global window, document */ + +(function () { + 'use strict'; + + function getSectok() { + try { + if (window.JSINFO && window.JSINFO.sectok) return String(window.JSINFO.sectok); + } catch (e) {} + + try { + var inp = document.querySelector('input[name="sectok"], input[name="securitytoken"]'); + if (inp && inp.value) return String(inp.value); + } catch (e2) {} + + return ''; + } + + function getPageId() { + try { + if (window.JSINFO && window.JSINFO.id) return String(window.JSINFO.id); + } catch (e) {} + + try { + var input = document.querySelector('input[name="id"]'); + if (input && input.value) return String(input.value); + } catch (e2) {} + + return ''; + } + + function getBaseUrl() { + try { + if (window.DOKU_BASE) return String(window.DOKU_BASE); + } catch (e) {} + + try { + if (window.JSINFO && window.JSINFO.base) return String(window.JSINFO.base); + } catch (e2) {} + + return '/'; + } + + function ensurePageLink() { + var pageId = getPageId(); + if (!pageId) return false; + + var endpoint = getBaseUrl() + 'lib/plugins/luxtools/pagelink.php'; + + var params = new window.URLSearchParams(); + params.set('cmd', 'ensure'); + params.set('id', pageId); + params.set('sectok', getSectok()); + + window.fetch(endpoint, { + method: 'POST', + credentials: 'same-origin', + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, + body: params.toString() + }).then(function (res) { + return res.json().catch(function () { return null; }).then(function (body) { + if (!res.ok || !body || body.ok !== true) { + throw new Error('request failed'); + } + return body; + }); + }).catch(function (err) { + if (window.console && window.console.warn) { + window.console.warn('PageLink creation failed:', err); + } + }); + + return false; + } + + function copyToClipboard(text) { + if (!text) return; + if (window.navigator && window.navigator.clipboard && window.navigator.clipboard.writeText) { + window.navigator.clipboard.writeText(text).catch(function () {}); + return; + } + + try { + var textarea = document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', 'readonly'); + textarea.style.position = 'absolute'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + try { + document.execCommand('copy'); + } catch (e) {} + document.body.removeChild(textarea); + } catch (e2) {} + } + + function attachStatus() { + var status = document.querySelector('.luxtools-pagelink-status[data-luxtools-pagelink="1"]'); + if (!status) return; + + var pageTitle = document.querySelector('#dokuwiki__content h1') + || document.querySelector('.pageId') + || document.querySelector('h1'); + + if (pageTitle && pageTitle.appendChild) { + status.classList.add('is-inline'); + pageTitle.appendChild(status); + } + + var copy = String(status.getAttribute('data-copy') || '') === '1'; + if (copy) { + status.setAttribute('role', 'button'); + status.setAttribute('tabindex', '0'); + status.addEventListener('click', function (e) { + e.preventDefault(); + copyToClipboard(String(status.getAttribute('data-uuid') || '').trim()); + }); + status.addEventListener('keydown', function (e) { + if (!e || (e.key !== 'Enter' && e.key !== ' ')) return; + e.preventDefault(); + copyToClipboard(String(status.getAttribute('data-uuid') || '').trim()); + }); + } + } + + window.addBtnActionLuxtoolsPageLink = function ($btn, props, edid) { + $btn.on('click', function () { + ensurePageLink(); + return false; + }); + return 'luxtools-pagelink'; + }; + + document.addEventListener('DOMContentLoaded', attachStatus, false); +})(); diff --git a/lang/de/lang.php b/lang/de/lang.php index dd7a657..f63fe0f 100644 --- a/lang/de/lang.php +++ b/lang/de/lang.php @@ -62,6 +62,8 @@ $lang["gallery_thumb_scale"] = "Skalierungsfaktor für Galerie-Thumbnails. 2 erzeugt schärfere Thumbnails auf HiDPI-Displays (Anzeige bleibt 150×150)."; $lang["open_service_url"] = "URL des lokalen Client-Dienstes für {{open>...}} (z.B. http://127.0.0.1:8765)."; +$lang["pagelink_search_depth"] = + "Maximale Verzeichnisebene für .pagelink-Suche (0 = nur Root)."; $lang["scratchpad_edit"] = "Scratchpad bearbeiten"; $lang["scratchpad_save"] = "Speichern"; @@ -73,3 +75,8 @@ $lang["scratchpad_err_unreadable"] = "Scratchpad-Datei ist nicht lesbar"; $lang["toolbar_code_title"] = "Code-Block"; $lang["toolbar_code_sample"] = "Ihr Code hier"; +$lang["toolbar_datefix_title"] = "Datums-Fix"; +$lang["toolbar_datefix_all_title"] = "Datums-Fix (Alle)"; +$lang["toolbar_pagelink_title"] = "Seiten-Link"; +$lang["pagelink_unlinked"] = "Nicht verknüpft: ID kopieren"; +$lang["pagelink_multi_warning"] = "Mehrere Ordner verknüpft"; diff --git a/lang/de/settings.php b/lang/de/settings.php index 0eb9e73..d5b1d13 100644 --- a/lang/de/settings.php +++ b/lang/de/settings.php @@ -26,3 +26,5 @@ $lang["thumb_placeholder"] = "MediaManager-ID fuer den Platzhalter der Galerie-T $lang["gallery_thumb_scale"] = "Skalierungsfaktor fuer Galerie-Thumbnails. 2 erzeugt schaerfere Thumbnails auf HiDPI-Displays (Anzeige bleibt 150x150)."; $lang["open_service_url"] = "URL des lokalen Client-Dienstes fuer {{open>...}} (z.B. http://127.0.0.1:8765)."; + +$lang["pagelink_search_depth"] = "Maximale Verzeichnisebene fuer .pagelink-Suche (0 = nur Root)."; diff --git a/lang/en/lang.php b/lang/en/lang.php index 9c42cb7..d8f1c3c 100644 --- a/lang/en/lang.php +++ b/lang/en/lang.php @@ -62,6 +62,8 @@ $lang["gallery_thumb_scale"] = "Gallery thumbnail scale factor. Use 2 for sharper thumbnails on HiDPI screens (still displayed as 150×150)."; $lang["open_service_url"] = "Local client service URL for the {{open>...}} button (e.g. http://127.0.0.1:8765)."; +$lang["pagelink_search_depth"] = + "Maximum directory depth for .pagelink search (0 = only root)."; $lang["scratchpad_edit"] = "Edit scratchpad"; $lang["scratchpad_save"] = "Save"; @@ -75,3 +77,6 @@ $lang["toolbar_code_title"] = "Code Block"; $lang["toolbar_code_sample"] = "your code here"; $lang["toolbar_datefix_title"] = "Date Fix"; $lang["toolbar_datefix_all_title"] = "Date Fix (All)"; +$lang["toolbar_pagelink_title"] = "Page Link"; +$lang["pagelink_unlinked"] = "Not linked: Copy ID"; +$lang["pagelink_multi_warning"] = "Multiple folders linked"; diff --git a/lang/en/settings.php b/lang/en/settings.php index a3c7507..cdc04a0 100644 --- a/lang/en/settings.php +++ b/lang/en/settings.php @@ -26,3 +26,5 @@ $lang['thumb_placeholder'] = 'MediaManager ID for the gallery thumbnail placehol $lang['gallery_thumb_scale'] = 'Gallery thumbnail scale factor. Use 2 for sharper thumbnails on HiDPI screens (still displayed as 150×150).'; $lang['open_service_url'] = 'Local client service URL for the {{open>...}} link (e.g. http://127.0.0.1:8765).'; + +$lang['pagelink_search_depth'] = 'Maximum directory depth for .pagelink search (0 = only root).'; diff --git a/pagelink.php b/pagelink.php new file mode 100644 index 0000000..5faa4bf --- /dev/null +++ b/pagelink.php @@ -0,0 +1,86 @@ + false, 'error' => 'plugin disabled']); + exit; +} + +/** + * Send a JSON response. + * + * @param int $status + * @param array $payload + * @return void + */ +function luxtools_pagelink_json(int $status, array $payload): void +{ + http_status($status); + header('Content-Type: application/json; charset=utf-8'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Pragma: no-cache'); + echo json_encode($payload); + exit; +} + +$cmd = (string)$INPUT->str('cmd'); +$pageId = (string)$INPUT->str('id'); +if (function_exists('cleanID')) { + $pageId = (string)cleanID($pageId); +} + +if ($cmd === '' || $pageId === '') { + luxtools_pagelink_json(400, ['ok' => false, 'error' => 'missing parameters']); +} + +if (!function_exists('auth_quickaclcheck') || !defined('AUTH_EDIT')) { + luxtools_pagelink_json(403, ['ok' => false, 'error' => 'forbidden']); +} + +if (auth_quickaclcheck($pageId) < AUTH_EDIT) { + luxtools_pagelink_json(403, ['ok' => false, 'error' => 'forbidden']); +} + +if ($cmd === 'ensure') { + if (strtoupper($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { + luxtools_pagelink_json(405, ['ok' => false, 'error' => 'method not allowed']); + } + + if (!checkSecurityToken()) { + luxtools_pagelink_json(403, ['ok' => false, 'error' => 'bad token']); + } + + $depth = (int)$syntax->getConf('pagelink_search_depth'); + if ($depth < 0) $depth = 0; + + $pageLink = new PageLink((string)$syntax->getConf('paths'), $depth); + $uuid = $pageLink->getPageUuid($pageId); + + if ($uuid !== null) { + luxtools_pagelink_json(200, ['ok' => true, 'uuid' => $uuid, 'created' => false]); + } + + $uuid = PageLink::createUuidV4(); + $ok = $pageLink->setPageUuid($pageId, $uuid); + + if (!$ok) { + luxtools_pagelink_json(500, ['ok' => false, 'error' => 'save failed']); + } + + luxtools_pagelink_json(200, ['ok' => true, 'uuid' => $uuid, 'created' => true]); +} + +luxtools_pagelink_json(400, ['ok' => false, 'error' => 'unknown command']); diff --git a/src/PageLink.php b/src/PageLink.php new file mode 100644 index 0000000..0e6aec2 --- /dev/null +++ b/src/PageLink.php @@ -0,0 +1,296 @@ +pathConfig = $pathConfig; + $this->maxDepth = max(0, $maxDepth); + } + + /** + * Generate a v4 UUID (lowercase). + */ + public static function createUuidV4(): string + { + $bytes = random_bytes(16); + $bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40); + $bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80); + $hex = bin2hex($bytes); + return sprintf( + '%s-%s-%s-%s-%s', + substr($hex, 0, 8), + substr($hex, 8, 4), + substr($hex, 12, 4), + substr($hex, 16, 4), + substr($hex, 20, 12) + ); + } + + /** + * Normalize and validate a UUID v4 string. + * + * @param string $uuid + * @return string|null + */ + public static function normalizeUuid(string $uuid): ?string + { + $uuid = strtolower(trim($uuid)); + if ($uuid === '') return null; + + if (!preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/', $uuid)) { + return null; + } + return $uuid; + } + + /** + * Get the page's pagelink UUID from metadata (if valid). + */ + public function getPageUuid(string $pageId): ?string + { + if ($pageId === '') return null; + if (!function_exists('p_get_metadata')) return null; + + $value = p_get_metadata($pageId, self::META_KEY, METADATA_DONT_RENDER); + if (!is_string($value)) return null; + return self::normalizeUuid($value); + } + + /** + * Persist a pagelink UUID in page metadata. + */ + public function setPageUuid(string $pageId, string $uuid): bool + { + if ($pageId === '') return false; + if (!function_exists('p_set_metadata')) return false; + $uuid = self::normalizeUuid($uuid); + if ($uuid === null) return false; + + return (bool)p_set_metadata($pageId, [self::META_KEY => $uuid]); + } + + /** + * Resolve a pagelink UUID to a linked folder (if any). + * + * @param string $uuid + * @return array{folder: string|null, multiple: bool} + */ + public function resolveUuid(string $uuid): array + { + $uuid = self::normalizeUuid($uuid); + if ($uuid === null) { + return ['folder' => null, 'multiple' => false]; + } + + $cache = $this->loadCache(); + if (isset($cache[$uuid]) && is_string($cache[$uuid]) && $cache[$uuid] !== '') { + $cachedPath = $cache[$uuid]; + if ($this->isValidLink($cachedPath, $uuid)) { + return ['folder' => $cachedPath, 'multiple' => false]; + } + + unset($cache[$uuid]); + $this->cacheDirty = true; + } + + $matches = $this->scanRootsForUuid($uuid, 2); + if ($matches !== []) { + $cache[$uuid] = $matches[0]; + $this->cacheDirty = true; + } + + if ($this->cacheDirty) { + $this->writeCache($cache); + } + + return [ + 'folder' => $matches[0] ?? null, + 'multiple' => count($matches) > 1, + ]; + } + + /** + * Read the cache file into memory. + * + * @return array + */ + protected function loadCache(): array + { + if ($this->cache !== null) return $this->cache; + + $this->cache = []; + $file = $this->getCacheFile(); + if (!is_file($file) || !is_readable($file)) return $this->cache; + + $raw = @file_get_contents($file); + if (!is_string($raw) || $raw === '') return $this->cache; + + $decoded = json_decode($raw, true); + if (!is_array($decoded)) return $this->cache; + + $this->cache = $decoded; + return $this->cache; + } + + /** + * Write cache to disk atomically. + */ + protected function writeCache(array $cache): void + { + $file = $this->getCacheFile(); + $dir = dirname($file); + if (function_exists('io_mkdir_p')) { + io_mkdir_p($dir); + } elseif (!@is_dir($dir)) { + @mkdir($dir, 0777, true); + } + + $tmp = $file . '.tmp.' . getmypid(); + $data = json_encode($cache, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + if ($data === false) return; + + @file_put_contents($tmp, $data, LOCK_EX); + if (!@rename($tmp, $file)) { + @copy($tmp, $file); + @unlink($tmp); + } + + $this->cache = $cache; + $this->cacheDirty = false; + } + + /** + * Get cache file path for pagelink mappings. + */ + protected function getCacheFile(): string + { + global $conf; + $cacheDir = rtrim((string)$conf['cachedir'], '/'); + return $cacheDir . '/luxtools/' . self::CACHE_FILE; + } + + /** + * Check if the cached path still points to a valid .pagelink file. + */ + protected function isValidLink(string $folder, string $uuid): bool + { + if ($folder === '') return false; + if (!is_dir($folder)) return false; + if (is_link($folder)) return false; + + $file = rtrim($folder, '/\\') . '/.pagelink'; + if (!is_file($file) || is_link($file) || !is_readable($file)) return false; + + $content = @file_get_contents($file); + if (!is_string($content)) return false; + + $content = self::normalizeUuid($content); + return $content !== null && $content === $uuid; + } + + /** + * Scan configured roots for matching .pagelink files. + * + * @param string $uuid + * @param int $limit Maximum number of matches to collect. + * @return string[] + */ + protected function scanRootsForUuid(string $uuid, int $limit = 2): array + { + $roots = $this->getConfiguredRoots(); + if ($roots === []) return []; + + $matches = []; + foreach ($roots as $root) { + $this->scanDirectory($root, 0, $uuid, $limit, $matches); + if (count($matches) >= $limit) break; + } + return $matches; + } + + /** + * Recursively scan a directory for .pagelink files. + * + * @param string $dir + * @param int $depth + * @param string $uuid + * @param int $limit + * @param array $matches + */ + protected function scanDirectory(string $dir, int $depth, string $uuid, int $limit, array &$matches): void + { + if ($dir === '' || count($matches) >= $limit) return; + if (!is_dir($dir) || is_link($dir)) return; + if (!is_readable($dir)) return; + if ($depth > $this->maxDepth) return; + + $file = rtrim($dir, '/\\') . '/.pagelink'; + if (is_file($file) && !is_link($file) && is_readable($file)) { + $content = @file_get_contents($file); + if (is_string($content)) { + $content = self::normalizeUuid($content); + if ($content !== null && $content === $uuid) { + $matches[] = rtrim($dir, '/\\'); + if (count($matches) >= $limit) return; + } + } + } + + if ($depth >= $this->maxDepth) return; + + $handle = @opendir($dir); + if ($handle === false) return; + + $base = rtrim($dir, '/\\'); + while (($entry = readdir($handle)) !== false) { + if ($entry === '.' || $entry === '..') continue; + if ($entry === '.pagelink') continue; + + $path = $base . '/' . $entry; + if (!is_dir($path) || is_link($path)) continue; + + $this->scanDirectory($path, $depth + 1, $uuid, $limit, $matches); + if (count($matches) >= $limit) break; + } + + closedir($handle); + } + + /** + * Resolve configured root paths (excluding aliases). + * + * @return string[] + */ + protected function getConfiguredRoots(): array + { + $pathConfig = trim($this->pathConfig); + if ($pathConfig === '') return []; + + $helper = new Path($pathConfig); + $paths = $helper->getPaths(); + $roots = []; + foreach ($paths as $key => $info) { + if (!isset($info['root']) || $key !== $info['root']) continue; + $roots[] = $info['root']; + } + return $roots; + } +} diff --git a/style.css b/style.css index 7b064bc..d8c09d7 100644 --- a/style.css +++ b/style.css @@ -49,6 +49,36 @@ div.luxtools-plugin .luxtools-empty { padding: 0.25em 0; } +/* Page link status (next to page title) */ +span.luxtools-pagelink-status { + display: inline-flex; + align-items: center; + gap: 0.35em; + font-size: 0.75em; + line-height: 1.2; + margin: 0.25em 0; + padding: 0.15em 0.4em; + border: 1px solid @ini_border; + border-radius: 0.2em; + background-color: @ini_background_alt; + color: inherit; +} + +span.luxtools-pagelink-status.is-inline { + margin-left: 0.5em; + margin-top: 0; + margin-bottom: 0; +} + +span.luxtools-pagelink-status[data-copy="1"] { + cursor: pointer; +} + +span.luxtools-pagelink-warning { + font-size: 0.95em; + opacity: 0.8; +} + /* Image gallery spacing. */ div.luxtools-gallery { padding-bottom: 0.5em; diff --git a/syntax/AbstractSyntax.php b/syntax/AbstractSyntax.php index 3aea4c5..66fe8ee 100644 --- a/syntax/AbstractSyntax.php +++ b/syntax/AbstractSyntax.php @@ -4,6 +4,7 @@ use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\plugin\luxtools\Crawler; use dokuwiki\plugin\luxtools\Output; use dokuwiki\plugin\luxtools\Path; +use dokuwiki\plugin\luxtools\PageLink; require_once(__DIR__ . '/../autoload.php'); @@ -208,7 +209,12 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin protected function getPathInfoSafe(string $basePath, \Doku_Renderer $renderer) { try { - $pathHelper = new Path($this->getConf('paths')); + $pathConfig = (string)$this->getConf('paths'); + $blobsRoot = $this->resolveBlobsRoot(); + if ($blobsRoot !== '') { + $pathConfig = rtrim($pathConfig) . "\n" . $blobsRoot . "\nA> blobs"; + } + $pathHelper = new Path($pathConfig); return $pathHelper->getPathInfo($basePath); } catch (\Exception $e) { $this->renderError($renderer, 'error_outsidejail'); @@ -216,6 +222,50 @@ abstract class syntax_plugin_luxtools_abstract extends SyntaxPlugin } } + /** + * Resolve the current page's pagelink folder for the blobs alias. + * + * @return string + */ + protected function resolveBlobsRoot(): string + { + static $cached = []; + + global $ID; + $pageId = is_string($ID) ? $ID : ''; + if ($pageId === '') return ''; + + if (function_exists('cleanID')) { + $pageId = (string)cleanID($pageId); + } + if ($pageId === '') return ''; + + if (isset($cached[$pageId])) { + return (string)$cached[$pageId]; + } + + $pathConfig = (string)$this->getConf('paths'); + $depth = (int)$this->getConf('pagelink_search_depth'); + if ($depth < 0) $depth = 0; + + $pageLink = new PageLink($pathConfig, $depth); + $uuid = $pageLink->getPageUuid($pageId); + if ($uuid === null) { + $cached[$pageId] = ''; + return ''; + } + + $linkInfo = $pageLink->resolveUuid($uuid); + $folder = $linkInfo['folder'] ?? ''; + if (!is_string($folder) || $folder === '') { + $cached[$pageId] = ''; + return ''; + } + + $cached[$pageId] = $folder; + return $folder; + } + /** * Create and configure a Crawler instance. * diff --git a/syntax/image.php b/syntax/image.php index 849cdb9..1992c82 100644 --- a/syntax/image.php +++ b/syntax/image.php @@ -2,6 +2,7 @@ use dokuwiki\Extension\SyntaxPlugin; use dokuwiki\plugin\luxtools\Path; +use dokuwiki\plugin\luxtools\PageLink; use dokuwiki\plugin\luxtools\ThumbnailHelper; require_once(__DIR__ . '/../autoload.php'); @@ -132,7 +133,7 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin } try { - $pathHelper = new Path($this->getConf('paths')); + $pathHelper = new Path($this->buildPathConfigWithBlobs()); // Use addTrailingSlash=false since this is a file path, not a directory $pathInfo = $pathHelper->getPathInfo($data['path'], false); } catch (\Exception $e) { @@ -214,6 +215,60 @@ class syntax_plugin_luxtools_image extends SyntaxPlugin return DOKU_BASE . 'lib/plugins/luxtools/file.php?' . http_build_query($params, '', '&'); } + /** + * Build a path configuration string, adding the blobs alias if available. + */ + protected function buildPathConfigWithBlobs(): string + { + $pathConfig = (string)$this->getConf('paths'); + $blobsRoot = $this->resolveBlobsRoot(); + if ($blobsRoot !== '') { + $pathConfig = rtrim($pathConfig) . "\n" . $blobsRoot . "\nA> blobs"; + } + return $pathConfig; + } + + /** + * Resolve the current page's pagelink folder for the blobs alias. + */ + protected function resolveBlobsRoot(): string + { + static $cached = []; + + global $ID; + $pageId = is_string($ID) ? $ID : ''; + if ($pageId === '') return ''; + + if (function_exists('cleanID')) { + $pageId = (string)cleanID($pageId); + } + if ($pageId === '') return ''; + + if (isset($cached[$pageId])) { + return (string)$cached[$pageId]; + } + + $depth = (int)$this->getConf('pagelink_search_depth'); + if ($depth < 0) $depth = 0; + + $pageLink = new PageLink((string)$this->getConf('paths'), $depth); + $uuid = $pageLink->getPageUuid($pageId); + if ($uuid === null) { + $cached[$pageId] = ''; + return ''; + } + + $linkInfo = $pageLink->resolveUuid($uuid); + $folder = $linkInfo['folder'] ?? ''; + if (!is_string($folder) || $folder === '') { + $cached[$pageId] = ''; + return ''; + } + + $cached[$pageId] = $folder; + return $folder; + } + /** * Render the imagebox HTML. *