From c442c0df1e92763a2d92ac302c65ede21347118a Mon Sep 17 00:00:00 2001 From: luxick Date: Mon, 5 Jan 2026 11:25:18 +0100 Subject: [PATCH] Inial implementation for folder opening --- _test/SyntaxTest.php | 20 +++++++++++ script.js | 58 ++++++++++++++++++++++++++++++++ syntax/open.php | 79 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 157 insertions(+) create mode 100644 script.js create mode 100644 syntax/open.php diff --git a/_test/SyntaxTest.php b/_test/SyntaxTest.php index 7a1024a..924570c 100644 --- a/_test/SyntaxTest.php +++ b/_test/SyntaxTest.php @@ -213,4 +213,24 @@ class plugin_filetools_test extends DokuWikiTest $this->assertStringContainsString('exampleimage.png', $xhtml); } + + /** + * This function checks that the open syntax renders an inline button. + */ + public function test_open_button() + { + $instructions = p_get_instructions('{{open>/tmp/somewhere|Open here}}'); + $xhtml = p_render('xhtml', $instructions, $info); + + $doc = new Document(); + $doc->html($xhtml); + + $structure = [ + 'button.filetools-open' => 1, + ]; + $this->structureCheck($doc, $structure); + + $this->assertStringContainsString('Open here', $xhtml); + $this->assertStringContainsString('data-path="/tmp/somewhere"', $xhtml); + } } diff --git a/script.js b/script.js new file mode 100644 index 0000000..ffb6d8d --- /dev/null +++ b/script.js @@ -0,0 +1,58 @@ +/* global window, document */ + +(function () { + 'use strict'; + + function normalizeToFileUrl(path) { + if (!path) return ''; + + // already a file URL + if (/^file:\/\//i.test(path)) return path; + + // UNC path: \\server\share\path + if (/^\\\\/.test(path)) { + var p = path.replace(/^\\\\/, ''); + p = p.replace(/\\/g, '/'); + return 'file://///' + p; + } + + // Windows drive: C:\path\to\file + if (/^[a-zA-Z]:\\/.test(path)) { + var drive = path[0].toUpperCase(); + var rest = path.slice(2).replace(/\\/g, '/'); + return 'file:///' + drive + ':' + rest; + } + + // POSIX absolute: /home/user/file + if (path[0] === '/') { + return 'file://' + path; + } + + // Fall back to using the provided string. + return path; + } + + function onClick(event) { + var el = event.target; + if (!el || !el.classList || !el.classList.contains('filetools-open')) return; + + var raw = el.getAttribute('data-path') || ''; + var url = normalizeToFileUrl(raw); + console.log('Opening file URL:', url); + if (!url) return; + + // Best-effort: browsers may block file:// navigation depending on settings. + try { + window.open(url, '_blank', 'noopener'); + } catch (e) { + console.error('Failed to open file URL in new tab:', e); + try { + window.location.href = url; + } catch (e2) { + console.error('Failed to open file URL:', e2); + } + } + } + + document.addEventListener('click', onClick, false); +})(); diff --git a/syntax/open.php b/syntax/open.php new file mode 100644 index 0000000..374e5e4 --- /dev/null +++ b/syntax/open.php @@ -0,0 +1,79 @@ +Lexer->addSpecialPattern('\{\{open>.+?\}\}', $mode, 'plugin_filetools_open'); + } + + /** @inheritdoc */ + public function handle($match, $state, $pos, Doku_Handler $handler) + { + $match = substr($match, strlen('{{open>'), -2); + [$path, $caption] = array_pad(explode('|', $match, 2), 2, ''); + + $path = trim($path); + $caption = trim($caption); + if ($caption === '') $caption = $path !== '' ? $path : 'Open'; + + // Basic scheme filtering to avoid javascript: style injections. + // Allow either file:// URLs, or plain paths (Windows/UNC/Linux style). + if (preg_match('/^[a-zA-Z][a-zA-Z0-9+.-]*:/', $path)) { + if (!str_starts_with(strtolower($path), 'file://')) { + return false; + } + } + + return [$path, $caption]; + } + + /** @inheritdoc */ + public function render($format, Doku_Renderer $renderer, $data) + { + if ($data === false) return false; + [$path, $caption] = $data; + + if ($format !== 'xhtml') { + // no meaningful representation in non-browser formats + $renderer->cdata($caption); + return true; + } + + if ($path === '') { + $renderer->cdata('[n/a]'); + return true; + } + + $renderer->doc .= ''; + return true; + } +}