146a60b4fSRobertWeinmeister<?php 246a60b4fSRobertWeinmeister/** 346a60b4fSRobertWeinmeister * DokuWiki Plugin mermaid (Action Component) 446a60b4fSRobertWeinmeister * 546a60b4fSRobertWeinmeister * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 646a60b4fSRobertWeinmeister * @author Robert Weinmeister <develop@weinmeister.org> 746a60b4fSRobertWeinmeister */ 846a60b4fSRobertWeinmeister 9b566ae41SRobert Weinmeisterdeclare(strict_types=1); 10b566ae41SRobert Weinmeister 11b566ae41SRobert Weinmeisterif (!defined('DOKU_INC')) { 12b566ae41SRobert Weinmeister die(); 13b566ae41SRobert Weinmeister} 14ea08b541SRobert Weinmeister 1546a60b4fSRobertWeinmeisterclass action_plugin_mermaid extends \dokuwiki\Extension\ActionPlugin 1646a60b4fSRobertWeinmeister{ 1746a60b4fSRobertWeinmeister /** @inheritDoc */ 18b566ae41SRobert Weinmeister public function register(Doku_Event_Handler $controller): void { 1946a60b4fSRobertWeinmeister $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load'); 20172fa282SRobert Weinmeister $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxRequest'); 21172fa282SRobert Weinmeister } 22172fa282SRobert Weinmeister 23b566ae41SRobert Weinmeister private function hasPermissionToEdit(string $ID): bool { 24b566ae41SRobert Weinmeister return auth_quickaclcheck($ID) >= AUTH_EDIT; 25172fa282SRobert Weinmeister } 26172fa282SRobert Weinmeister 27b566ae41SRobert Weinmeister private function isPageLocked(string $ID): bool { 28b566ae41SRobert Weinmeister return checklock($ID); 29b566ae41SRobert Weinmeister } 30b566ae41SRobert Weinmeister 31b566ae41SRobert Weinmeister private function lockMermaidDiagram(string $wikitext): string { 32b566ae41SRobert Weinmeister preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE); 33b566ae41SRobert Weinmeister 34b566ae41SRobert Weinmeister if (is_array($matches) && count($matches[0]) > (int)$_REQUEST['mermaidindex']) { 35b566ae41SRobert Weinmeister $whereToInsert = $matches[1][(int)$_REQUEST['mermaidindex']][1]; 36b566ae41SRobert Weinmeister return substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert); 37b566ae41SRobert Weinmeister } 38b566ae41SRobert Weinmeister 39b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]); 40b566ae41SRobert Weinmeister exit(); 41b566ae41SRobert Weinmeister } 42b566ae41SRobert Weinmeister 43b566ae41SRobert Weinmeister private function unlockMermaidDiagram(string $wikitext): string { 44b566ae41SRobert Weinmeister $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count); 45b566ae41SRobert Weinmeister 46b566ae41SRobert Weinmeister if ($count !== 1) { 47b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]); 48b566ae41SRobert Weinmeister exit(); 49b566ae41SRobert Weinmeister } 50b566ae41SRobert Weinmeister 51b566ae41SRobert Weinmeister return $newWikitext; 52b566ae41SRobert Weinmeister } 53b566ae41SRobert Weinmeister 54b566ae41SRobert Weinmeister private function isWikiTextChanged(string $wikitext, string $newWikitext): bool { 55b566ae41SRobert Weinmeister return strlen($newWikitext) > 0 && $newWikitext !== $wikitext; 56b566ae41SRobert Weinmeister } 57b566ae41SRobert Weinmeister 58b566ae41SRobert Weinmeister private function saveWikiChanges(string $ID, string $newWikitext, string $mode): void { 59b566ae41SRobert Weinmeister lock($ID); 60b566ae41SRobert Weinmeister saveWikiText($ID, $newWikitext, "{$mode} Mermaid diagram", true); 61b566ae41SRobert Weinmeister unlock($ID); 62b566ae41SRobert Weinmeister } 63b566ae41SRobert Weinmeister 64b566ae41SRobert Weinmeister public function handleAjaxRequest(Doku_Event $event, $param): void { 65b566ae41SRobert Weinmeister if ($event->data !== 'plugin_mermaid') { 66b566ae41SRobert Weinmeister return; 67b566ae41SRobert Weinmeister } 68172fa282SRobert Weinmeister $event->stopPropagation(); 69172fa282SRobert Weinmeister $event->preventDefault(); 70172fa282SRobert Weinmeister 71b566ae41SRobert Weinmeister if (!isset($_REQUEST['mermaidindex']) || !isset($_REQUEST['svg'])) { 72b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Missing required parameters.']]); 73b566ae41SRobert Weinmeister exit(); 74b566ae41SRobert Weinmeister } 75b566ae41SRobert Weinmeister 76172fa282SRobert Weinmeister $ID = cleanID(urldecode($_REQUEST['pageid'])); 77172fa282SRobert Weinmeister 78b566ae41SRobert Weinmeister if(!$this->hasPermissionToEdit($ID)) { 79172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['You do not have permission to edit this file.\nAccess was denied.']]); 80172fa282SRobert Weinmeister exit(); 81172fa282SRobert Weinmeister } 82172fa282SRobert Weinmeister 83b566ae41SRobert Weinmeister if($this->isPageLocked($ID)) { 84172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['The page is currently locked.\nTry again later.']]); 85172fa282SRobert Weinmeister exit(); 86172fa282SRobert Weinmeister } 87172fa282SRobert Weinmeister 88172fa282SRobert Weinmeister $wikitext = rawWiki($ID); 89172fa282SRobert Weinmeister $newWikitext = $wikitext; 90172fa282SRobert Weinmeister 91b566ae41SRobert Weinmeister if($_REQUEST['mode'] === 'lock') { 92172fa282SRobert Weinmeister preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE); 93172fa282SRobert Weinmeister 94172fa282SRobert Weinmeister if(is_array($matches) && count($matches[0]) > $_REQUEST['mermaidindex']) 95172fa282SRobert Weinmeister { 96172fa282SRobert Weinmeister $whereToInsert = $matches[1][$_REQUEST['mermaidindex']][1]; 97172fa282SRobert Weinmeister $newWikitext = substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert); 98172fa282SRobert Weinmeister } 99172fa282SRobert Weinmeister else 100172fa282SRobert Weinmeister { 101172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]); 102172fa282SRobert Weinmeister exit(); 103172fa282SRobert Weinmeister } 104172fa282SRobert Weinmeister } 105172fa282SRobert Weinmeister 106172fa282SRobert Weinmeister if($_REQUEST['mode'] == 'unlock') 107172fa282SRobert Weinmeister { 108172fa282SRobert Weinmeister $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count); 109172fa282SRobert Weinmeister if($count != 1) 110172fa282SRobert Weinmeister { 111172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]); 112172fa282SRobert Weinmeister exit(); 113172fa282SRobert Weinmeister } 114172fa282SRobert Weinmeister } 115172fa282SRobert Weinmeister 116b566ae41SRobert Weinmeister if($this->isWikiTextChanged($wikitext, $newWikitext)) { 117b566ae41SRobert Weinmeister $this->saveWikiChanges($ID, $newWikitext, $_REQUEST['mode']); 118172fa282SRobert Weinmeister echo json_encode(['status' => 'success', 'data' => []]); 119b566ae41SRobert Weinmeister } else{ 120172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not ' . $_REQUEST['mode'] . ' the Mermaid diagram.']]); 121b566ae41SRobert Weinmeister } 122b566ae41SRobert Weinmeister 123172fa282SRobert Weinmeister exit(); 12446a60b4fSRobertWeinmeister } 12546a60b4fSRobertWeinmeister 126b566ae41SRobert Weinmeister private function addLocalScript(Doku_Event $event): void { 127b566ae41SRobert Weinmeister $event->data['script'][] = [ 128b566ae41SRobert Weinmeister 'type' => 'text/javascript', 129b566ae41SRobert Weinmeister 'charset' => 'utf-8', 130b566ae41SRobert Weinmeister 'src' => DOKU_BASE . 'lib/plugins/mermaid/mermaid.min.js', 131b566ae41SRobert Weinmeister ]; 132b566ae41SRobert Weinmeister } 133b566ae41SRobert Weinmeister 134b566ae41SRobert Weinmeister private function addEsmScript(Doku_Event $event, string $version, string $init): void { 135b566ae41SRobert Weinmeister $data = "import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid{$version}/dist/mermaid.esm.min.mjs';{$init}"; 136b566ae41SRobert Weinmeister $event->data['script'][] = [ 137b566ae41SRobert Weinmeister 'type' => 'module', 138b566ae41SRobert Weinmeister 'charset' => 'utf-8', 139b566ae41SRobert Weinmeister '_data' => $data, 140b566ae41SRobert Weinmeister ]; 141b566ae41SRobert Weinmeister } 142b566ae41SRobert Weinmeister 143b566ae41SRobert Weinmeister private function addScript(Doku_Event $event, string $version, string $init): void { 144b566ae41SRobert Weinmeister $event->data['script'][] = [ 145b566ae41SRobert Weinmeister 'type' => 'text/javascript', 146b566ae41SRobert Weinmeister 'charset' => 'utf-8', 147b566ae41SRobert Weinmeister 'src' => "https://cdn.jsdelivr.net/npm/mermaid{$version}/dist/mermaid.min.js", 148b566ae41SRobert Weinmeister ]; 149b566ae41SRobert Weinmeister 150b566ae41SRobert Weinmeister $event->data['script'][] = [ 151b566ae41SRobert Weinmeister 'type' => 'text/javascript', 152b566ae41SRobert Weinmeister 'charset' => 'utf-8', 153b566ae41SRobert Weinmeister '_data' => $init, 154b566ae41SRobert Weinmeister ]; 155b566ae41SRobert Weinmeister } 156b566ae41SRobert Weinmeister 157*b74fbfffSRobert Weinmeister private function pageIncludesMermaid(): bool { 158*b74fbfffSRobert Weinmeister // true if the mermaid tag is used 159*b74fbfffSRobert Weinmeister // the include plugin can hide this fact, so we need a separate check for it 160*b74fbfffSRobert Weinmeister $wikiText = rawWiki(getID()); 161*b74fbfffSRobert Weinmeister if (str_contains($wikiText, '<mermaid') || str_contains($wikiText, '{{page>') || str_contains($wikiText, '{{section>') || str_contains($wikiText, '{{namespace>') || str_contains($wikiText, '{{tagtopic>')) { 162*b74fbfffSRobert Weinmeister return true; 163*b74fbfffSRobert Weinmeister } 164*b74fbfffSRobert Weinmeister 165*b74fbfffSRobert Weinmeister return false; 166*b74fbfffSRobert Weinmeister } 167*b74fbfffSRobert Weinmeister 168b566ae41SRobert Weinmeister /** 169b566ae41SRobert Weinmeister * Load the Mermaid library and configuration into the page. 170b566ae41SRobert Weinmeister * 171b566ae41SRobert Weinmeister * @param Doku_Event $event DokuWiki event object 172b566ae41SRobert Weinmeister * @param mixed $param Unused parameter. 173b566ae41SRobert Weinmeister */ 174b566ae41SRobert Weinmeister public function load(Doku_Event $event, $param): void { 175ea08b541SRobert Weinmeister // only load mermaid if it is needed 176*b74fbfffSRobert Weinmeister if (!$this->pageIncludesMermaid()) { 177ea08b541SRobert Weinmeister return; 178ea08b541SRobert Weinmeister } 179ea08b541SRobert Weinmeister 1804c8bd9ffSRobert Weinmeister $theme = $this->getConf('theme'); 18155e3db93SRobert Weinmeister $look = $this->getConf('look'); 18255e3db93SRobert Weinmeister $logLevel = $this->getConf('logLevel'); 183b566ae41SRobert Weinmeister $init = "mermaid.initialize({startOnLoad: true, logLevel: '$logLevel', theme: '$theme', look: '$look'});"; 1844c8bd9ffSRobert Weinmeister 185b566ae41SRobert Weinmeister $location = $this->getConf('location'); 186b566ae41SRobert Weinmeister $versions = [ 1876fcac025SRobert Weinmeister 'latest' => '', 188a788b843SRobert Weinmeister 'remote1091' => '@10.9.1', 1896fcac025SRobert Weinmeister 'remote108' => '@10.8.0', 1906fcac025SRobert Weinmeister 'remote106' => '@10.6.1', 1916fcac025SRobert Weinmeister 'remote104' => '@10.4.0', 1926fcac025SRobert Weinmeister 'remote103' => '@10.3.1', 1936fcac025SRobert Weinmeister 'remote102' => '@10.2.4', 1946fcac025SRobert Weinmeister 'remote101' => '@10.1.0', 195b566ae41SRobert Weinmeister 'remote100' => '@10.0.2', 196b566ae41SRobert Weinmeister 'remote94' => '@9.4.3', 197b566ae41SRobert Weinmeister 'remote943' => '@9.4.3', 198b566ae41SRobert Weinmeister 'remote93' => '@9.3.0', 199b566ae41SRobert Weinmeister ]; 20046a60b4fSRobertWeinmeister 201b566ae41SRobert Weinmeister // add the appropriate Mermaid script based on the location configuration 202b566ae41SRobert Weinmeister match ($location) { 203b566ae41SRobert Weinmeister 'local' => $this->addLocalScript($event), 204b566ae41SRobert Weinmeister 'latest', 'remote1091', 'remote108', 'remote106', 'remote104', 'remote103', 'remote102', 'remote101', 'remote100' 205b566ae41SRobert Weinmeister => $this->addEsmScript($event, $versions[$location], $init), 206b566ae41SRobert Weinmeister 'remote94', 'remote943', 'remote93' 207b566ae41SRobert Weinmeister => $this->addScript($event, $versions[$location], $init), 208b566ae41SRobert Weinmeister default => null, 209b566ae41SRobert Weinmeister }; 210b566ae41SRobert Weinmeister 211b566ae41SRobert Weinmeister $event->data['link'][] = [ 21246a60b4fSRobertWeinmeister 'rel' => 'stylesheet', 21346a60b4fSRobertWeinmeister 'type' => 'text/css', 21446a60b4fSRobertWeinmeister 'href' => DOKU_BASE . "lib/plugins/mermaid/mermaid.css", 215b566ae41SRobert Weinmeister ]; 2166fcac025SRobert Weinmeister 2176fcac025SRobert Weinmeister // remove the search highlight from DokuWiki as it interferes with the Mermaid parsing/rendering 218b566ae41SRobert Weinmeister $event->data['script'][] = [ 2196fcac025SRobert Weinmeister 'type' => 'text/javascript', 2206fcac025SRobert Weinmeister 'charset' => 'utf-8', 2216fcac025SRobert Weinmeister '_data' => "document.addEventListener('DOMContentLoaded', function() { 2226fcac025SRobert Weinmeister jQuery('.mermaid').each(function() { 2236fcac025SRobert Weinmeister var modifiedContent = jQuery(this).html().replace(/<span class=\"search_hit\">(.+?)<\/span>/g, '$1'); 2246fcac025SRobert Weinmeister jQuery(this).html(modifiedContent); 2256fcac025SRobert Weinmeister }) 2266fcac025SRobert Weinmeister });" 227b566ae41SRobert Weinmeister ]; 2281da12d6eSRobert Weinmeister 2291da12d6eSRobert Weinmeister // adds image-save capability 2301da12d6eSRobert Weinmeister // First: Wait until the DOM content is fully loaded 2311da12d6eSRobert Weinmeister // Second: Wait until Mermaid has changed the dokuwiki content to an svg 2321da12d6eSRobert Weinmeister $event->data['script'][] = array 2331da12d6eSRobert Weinmeister ( 2341da12d6eSRobert Weinmeister 'type' => 'text/javascript', 2351da12d6eSRobert Weinmeister 'charset' => 'utf-8', 2361da12d6eSRobert Weinmeister '_data' => " 2371da12d6eSRobert Weinmeisterdocument.addEventListener('DOMContentLoaded', function() { 2381da12d6eSRobert Weinmeister var config = { 2391da12d6eSRobert Weinmeister childList: true, 2401da12d6eSRobert Weinmeister subtree: true, 2411da12d6eSRobert Weinmeister characterData: true 2421da12d6eSRobert Weinmeister }; 2431da12d6eSRobert Weinmeister 244172fa282SRobert Weinmeister function callDokuWikiPHP(mode, index, mermaidRaw, mermaidSvg) { 245172fa282SRobert Weinmeister jQuery.post( 246172fa282SRobert Weinmeister DOKU_BASE + 'lib/exe/ajax.php', 247172fa282SRobert Weinmeister { 248172fa282SRobert Weinmeister call: 'plugin_mermaid', 249172fa282SRobert Weinmeister mode: mode, 250172fa282SRobert Weinmeister mermaidindex: index, 251172fa282SRobert Weinmeister pageid: '".getID()."', 252172fa282SRobert Weinmeister svg: encodeURIComponent(mermaidSvg) 253172fa282SRobert Weinmeister }, 254172fa282SRobert Weinmeister function(response) { 255172fa282SRobert Weinmeister if(response.status == 'success') { 256172fa282SRobert Weinmeister location.reload(true); 257172fa282SRobert Weinmeister } 258172fa282SRobert Weinmeister else { 259172fa282SRobert Weinmeister alert(response.data[0]); 260172fa282SRobert Weinmeister } 261172fa282SRobert Weinmeister }, 262172fa282SRobert Weinmeister 'json' 263172fa282SRobert Weinmeister )}; 264172fa282SRobert Weinmeister 265172fa282SRobert Weinmeister jQuery('.mermaidlocked, .mermaid').each(function(index, element) { 2661da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseenter', function() { 267172fa282SRobert Weinmeister document.getElementById('mermaidFieldset' + index).style.display = 'flex'; 2681da12d6eSRobert Weinmeister }); 2691da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseleave', function() { 270172fa282SRobert Weinmeister document.getElementById('mermaidFieldset' + index).style.display = 'none'; 2711da12d6eSRobert Weinmeister }); 2721da12d6eSRobert Weinmeister 273172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaidlocked')) { 274172fa282SRobert Weinmeister document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 2751da12d6eSRobert Weinmeister var svgContent = element.innerHTML.trim(); 2761da12d6eSRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 2771da12d6eSRobert Weinmeister var link = document.createElement('a'); 2781da12d6eSRobert Weinmeister link.href = URL.createObjectURL(blob); 2791da12d6eSRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 2801da12d6eSRobert Weinmeister link.click(); 2811da12d6eSRobert Weinmeister URL.revokeObjectURL(link.href); 2821da12d6eSRobert Weinmeister }); 283172fa282SRobert Weinmeister 284172fa282SRobert Weinmeister document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 285172fa282SRobert Weinmeister if(confirm('Unlock Mermaid diagram?')) { 286172fa282SRobert Weinmeister callDokuWikiPHP('unlock', index, originalMermaidContent, element.innerHTML.trim()); 287172fa282SRobert Weinmeister } 288172fa282SRobert Weinmeister }); 289172fa282SRobert Weinmeister } 290172fa282SRobert Weinmeister 291172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaid')) { 292172fa282SRobert Weinmeister var originalMermaidContent = element.innerHTML; 293172fa282SRobert Weinmeister var observer = new MutationObserver(function(mutations) { 294172fa282SRobert Weinmeister mutations.forEach(function(mutation) { 295172fa282SRobert Weinmeister if (mutation.type === 'childList' && element.innerHTML.startsWith('<svg')) { 296172fa282SRobert Weinmeister document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 297172fa282SRobert Weinmeister var svgContent = element.innerHTML.trim(); 298172fa282SRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 299172fa282SRobert Weinmeister var link = document.createElement('a'); 300172fa282SRobert Weinmeister link.href = URL.createObjectURL(blob); 301172fa282SRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 302172fa282SRobert Weinmeister link.click(); 303172fa282SRobert Weinmeister URL.revokeObjectURL(link.href); 304172fa282SRobert Weinmeister }); 305172fa282SRobert Weinmeister 306172fa282SRobert Weinmeister document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 307172fa282SRobert Weinmeister if(confirm('Lock Mermaid diagram? [experimental]')) { 308172fa282SRobert Weinmeister callDokuWikiPHP('lock', index, originalMermaidContent, element.innerHTML.trim()); 309172fa282SRobert Weinmeister } 310172fa282SRobert Weinmeister }); 3111da12d6eSRobert Weinmeister } 3121da12d6eSRobert Weinmeister }); 3131da12d6eSRobert Weinmeister }); 3141da12d6eSRobert Weinmeister observer.observe(element, config); 315172fa282SRobert Weinmeister } 3161da12d6eSRobert Weinmeister }); 3171da12d6eSRobert Weinmeister});" 3181da12d6eSRobert Weinmeister ); 31946a60b4fSRobertWeinmeister } 32046a60b4fSRobertWeinmeister}