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 9*b566ae41SRobert Weinmeisterdeclare(strict_types=1); 10*b566ae41SRobert Weinmeister 11*b566ae41SRobert Weinmeisterif (!defined('DOKU_INC')) { 12*b566ae41SRobert Weinmeister die(); 13*b566ae41SRobert Weinmeister} 14ea08b541SRobert Weinmeister 1546a60b4fSRobertWeinmeisterclass action_plugin_mermaid extends \dokuwiki\Extension\ActionPlugin 1646a60b4fSRobertWeinmeister{ 1746a60b4fSRobertWeinmeister /** @inheritDoc */ 18*b566ae41SRobert 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 23*b566ae41SRobert Weinmeister private function hasPermissionToEdit(string $ID): bool { 24*b566ae41SRobert Weinmeister return auth_quickaclcheck($ID) >= AUTH_EDIT; 25172fa282SRobert Weinmeister } 26172fa282SRobert Weinmeister 27*b566ae41SRobert Weinmeister private function isPageLocked(string $ID): bool { 28*b566ae41SRobert Weinmeister return checklock($ID); 29*b566ae41SRobert Weinmeister } 30*b566ae41SRobert Weinmeister 31*b566ae41SRobert Weinmeister private function lockMermaidDiagram(string $wikitext): string { 32*b566ae41SRobert Weinmeister preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE); 33*b566ae41SRobert Weinmeister 34*b566ae41SRobert Weinmeister if (is_array($matches) && count($matches[0]) > (int)$_REQUEST['mermaidindex']) { 35*b566ae41SRobert Weinmeister $whereToInsert = $matches[1][(int)$_REQUEST['mermaidindex']][1]; 36*b566ae41SRobert Weinmeister return substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert); 37*b566ae41SRobert Weinmeister } 38*b566ae41SRobert Weinmeister 39*b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]); 40*b566ae41SRobert Weinmeister exit(); 41*b566ae41SRobert Weinmeister } 42*b566ae41SRobert Weinmeister 43*b566ae41SRobert Weinmeister private function unlockMermaidDiagram(string $wikitext): string { 44*b566ae41SRobert Weinmeister $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count); 45*b566ae41SRobert Weinmeister 46*b566ae41SRobert Weinmeister if ($count !== 1) { 47*b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]); 48*b566ae41SRobert Weinmeister exit(); 49*b566ae41SRobert Weinmeister } 50*b566ae41SRobert Weinmeister 51*b566ae41SRobert Weinmeister return $newWikitext; 52*b566ae41SRobert Weinmeister } 53*b566ae41SRobert Weinmeister 54*b566ae41SRobert Weinmeister private function isWikiTextChanged(string $wikitext, string $newWikitext): bool { 55*b566ae41SRobert Weinmeister return strlen($newWikitext) > 0 && $newWikitext !== $wikitext; 56*b566ae41SRobert Weinmeister } 57*b566ae41SRobert Weinmeister 58*b566ae41SRobert Weinmeister private function saveWikiChanges(string $ID, string $newWikitext, string $mode): void { 59*b566ae41SRobert Weinmeister lock($ID); 60*b566ae41SRobert Weinmeister saveWikiText($ID, $newWikitext, "{$mode} Mermaid diagram", true); 61*b566ae41SRobert Weinmeister unlock($ID); 62*b566ae41SRobert Weinmeister } 63*b566ae41SRobert Weinmeister 64*b566ae41SRobert Weinmeister public function handleAjaxRequest(Doku_Event $event, $param): void { 65*b566ae41SRobert Weinmeister if ($event->data !== 'plugin_mermaid') { 66*b566ae41SRobert Weinmeister return; 67*b566ae41SRobert Weinmeister } 68172fa282SRobert Weinmeister $event->stopPropagation(); 69172fa282SRobert Weinmeister $event->preventDefault(); 70172fa282SRobert Weinmeister 71*b566ae41SRobert Weinmeister if (!isset($_REQUEST['mermaidindex']) || !isset($_REQUEST['svg'])) { 72*b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Missing required parameters.']]); 73*b566ae41SRobert Weinmeister exit(); 74*b566ae41SRobert Weinmeister } 75*b566ae41SRobert Weinmeister 76172fa282SRobert Weinmeister $ID = cleanID(urldecode($_REQUEST['pageid'])); 77172fa282SRobert Weinmeister 78*b566ae41SRobert 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 83*b566ae41SRobert 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 91*b566ae41SRobert 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 116*b566ae41SRobert Weinmeister if($this->isWikiTextChanged($wikitext, $newWikitext)) { 117*b566ae41SRobert Weinmeister $this->saveWikiChanges($ID, $newWikitext, $_REQUEST['mode']); 118172fa282SRobert Weinmeister echo json_encode(['status' => 'success', 'data' => []]); 119*b566ae41SRobert Weinmeister } else{ 120172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not ' . $_REQUEST['mode'] . ' the Mermaid diagram.']]); 121*b566ae41SRobert Weinmeister } 122*b566ae41SRobert Weinmeister 123172fa282SRobert Weinmeister exit(); 12446a60b4fSRobertWeinmeister } 12546a60b4fSRobertWeinmeister 126*b566ae41SRobert Weinmeister private function addLocalScript(Doku_Event $event): void { 127*b566ae41SRobert Weinmeister $event->data['script'][] = [ 128*b566ae41SRobert Weinmeister 'type' => 'text/javascript', 129*b566ae41SRobert Weinmeister 'charset' => 'utf-8', 130*b566ae41SRobert Weinmeister 'src' => DOKU_BASE . 'lib/plugins/mermaid/mermaid.min.js', 131*b566ae41SRobert Weinmeister ]; 132*b566ae41SRobert Weinmeister } 133*b566ae41SRobert Weinmeister 134*b566ae41SRobert Weinmeister private function addEsmScript(Doku_Event $event, string $version, string $init): void { 135*b566ae41SRobert Weinmeister $data = "import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid{$version}/dist/mermaid.esm.min.mjs';{$init}"; 136*b566ae41SRobert Weinmeister $event->data['script'][] = [ 137*b566ae41SRobert Weinmeister 'type' => 'module', 138*b566ae41SRobert Weinmeister 'charset' => 'utf-8', 139*b566ae41SRobert Weinmeister '_data' => $data, 140*b566ae41SRobert Weinmeister ]; 141*b566ae41SRobert Weinmeister } 142*b566ae41SRobert Weinmeister 143*b566ae41SRobert Weinmeister private function addScript(Doku_Event $event, string $version, string $init): void { 144*b566ae41SRobert Weinmeister $event->data['script'][] = [ 145*b566ae41SRobert Weinmeister 'type' => 'text/javascript', 146*b566ae41SRobert Weinmeister 'charset' => 'utf-8', 147*b566ae41SRobert Weinmeister 'src' => "https://cdn.jsdelivr.net/npm/mermaid{$version}/dist/mermaid.min.js", 148*b566ae41SRobert Weinmeister ]; 149*b566ae41SRobert Weinmeister 150*b566ae41SRobert Weinmeister $event->data['script'][] = [ 151*b566ae41SRobert Weinmeister 'type' => 'text/javascript', 152*b566ae41SRobert Weinmeister 'charset' => 'utf-8', 153*b566ae41SRobert Weinmeister '_data' => $init, 154*b566ae41SRobert Weinmeister ]; 155*b566ae41SRobert Weinmeister } 156*b566ae41SRobert Weinmeister 157*b566ae41SRobert Weinmeister /** 158*b566ae41SRobert Weinmeister * Load the Mermaid library and configuration into the page. 159*b566ae41SRobert Weinmeister * 160*b566ae41SRobert Weinmeister * @param Doku_Event $event DokuWiki event object 161*b566ae41SRobert Weinmeister * @param mixed $param Unused parameter. 162*b566ae41SRobert Weinmeister */ 163*b566ae41SRobert Weinmeister public function load(Doku_Event $event, $param): void { 164ea08b541SRobert Weinmeister // only load mermaid if it is needed 165*b566ae41SRobert Weinmeister if (!str_contains(rawWiki(getID()), '<mermaid')) { 166ea08b541SRobert Weinmeister return; 167ea08b541SRobert Weinmeister } 168ea08b541SRobert Weinmeister 1694c8bd9ffSRobert Weinmeister $theme = $this->getConf('theme'); 17055e3db93SRobert Weinmeister $look = $this->getConf('look'); 17155e3db93SRobert Weinmeister $logLevel = $this->getConf('logLevel'); 172*b566ae41SRobert Weinmeister $init = "mermaid.initialize({startOnLoad: true, logLevel: '$logLevel', theme: '$theme', look: '$look'});"; 1734c8bd9ffSRobert Weinmeister 174*b566ae41SRobert Weinmeister $location = $this->getConf('location'); 175*b566ae41SRobert Weinmeister $versions = [ 1766fcac025SRobert Weinmeister 'latest' => '', 177a788b843SRobert Weinmeister 'remote1091' => '@10.9.1', 1786fcac025SRobert Weinmeister 'remote108' => '@10.8.0', 1796fcac025SRobert Weinmeister 'remote106' => '@10.6.1', 1806fcac025SRobert Weinmeister 'remote104' => '@10.4.0', 1816fcac025SRobert Weinmeister 'remote103' => '@10.3.1', 1826fcac025SRobert Weinmeister 'remote102' => '@10.2.4', 1836fcac025SRobert Weinmeister 'remote101' => '@10.1.0', 184*b566ae41SRobert Weinmeister 'remote100' => '@10.0.2', 185*b566ae41SRobert Weinmeister 'remote94' => '@9.4.3', 186*b566ae41SRobert Weinmeister 'remote943' => '@9.4.3', 187*b566ae41SRobert Weinmeister 'remote93' => '@9.3.0', 188*b566ae41SRobert Weinmeister ]; 18946a60b4fSRobertWeinmeister 190*b566ae41SRobert Weinmeister // add the appropriate Mermaid script based on the location configuration 191*b566ae41SRobert Weinmeister match ($location) { 192*b566ae41SRobert Weinmeister 'local' => $this->addLocalScript($event), 193*b566ae41SRobert Weinmeister 'latest', 'remote1091', 'remote108', 'remote106', 'remote104', 'remote103', 'remote102', 'remote101', 'remote100' 194*b566ae41SRobert Weinmeister => $this->addEsmScript($event, $versions[$location], $init), 195*b566ae41SRobert Weinmeister 'remote94', 'remote943', 'remote93' 196*b566ae41SRobert Weinmeister => $this->addScript($event, $versions[$location], $init), 197*b566ae41SRobert Weinmeister default => null, 198*b566ae41SRobert Weinmeister }; 199*b566ae41SRobert Weinmeister 200*b566ae41SRobert Weinmeister $event->data['link'][] = [ 20146a60b4fSRobertWeinmeister 'rel' => 'stylesheet', 20246a60b4fSRobertWeinmeister 'type' => 'text/css', 20346a60b4fSRobertWeinmeister 'href' => DOKU_BASE . "lib/plugins/mermaid/mermaid.css", 204*b566ae41SRobert Weinmeister ]; 2056fcac025SRobert Weinmeister 2066fcac025SRobert Weinmeister // remove the search highlight from DokuWiki as it interferes with the Mermaid parsing/rendering 207*b566ae41SRobert Weinmeister $event->data['script'][] = [ 2086fcac025SRobert Weinmeister 'type' => 'text/javascript', 2096fcac025SRobert Weinmeister 'charset' => 'utf-8', 2106fcac025SRobert Weinmeister '_data' => "document.addEventListener('DOMContentLoaded', function() { 2116fcac025SRobert Weinmeister jQuery('.mermaid').each(function() { 2126fcac025SRobert Weinmeister var modifiedContent = jQuery(this).html().replace(/<span class=\"search_hit\">(.+?)<\/span>/g, '$1'); 2136fcac025SRobert Weinmeister jQuery(this).html(modifiedContent); 2146fcac025SRobert Weinmeister }) 2156fcac025SRobert Weinmeister });" 216*b566ae41SRobert Weinmeister ]; 2171da12d6eSRobert Weinmeister 2181da12d6eSRobert Weinmeister // adds image-save capability 2191da12d6eSRobert Weinmeister // First: Wait until the DOM content is fully loaded 2201da12d6eSRobert Weinmeister // Second: Wait until Mermaid has changed the dokuwiki content to an svg 2211da12d6eSRobert Weinmeister $event->data['script'][] = array 2221da12d6eSRobert Weinmeister ( 2231da12d6eSRobert Weinmeister 'type' => 'text/javascript', 2241da12d6eSRobert Weinmeister 'charset' => 'utf-8', 2251da12d6eSRobert Weinmeister '_data' => " 2261da12d6eSRobert Weinmeisterdocument.addEventListener('DOMContentLoaded', function() { 2271da12d6eSRobert Weinmeister var config = { 2281da12d6eSRobert Weinmeister childList: true, 2291da12d6eSRobert Weinmeister subtree: true, 2301da12d6eSRobert Weinmeister characterData: true 2311da12d6eSRobert Weinmeister }; 2321da12d6eSRobert Weinmeister 233172fa282SRobert Weinmeister function callDokuWikiPHP(mode, index, mermaidRaw, mermaidSvg) { 234172fa282SRobert Weinmeister jQuery.post( 235172fa282SRobert Weinmeister DOKU_BASE + 'lib/exe/ajax.php', 236172fa282SRobert Weinmeister { 237172fa282SRobert Weinmeister call: 'plugin_mermaid', 238172fa282SRobert Weinmeister mode: mode, 239172fa282SRobert Weinmeister mermaidindex: index, 240172fa282SRobert Weinmeister pageid: '".getID()."', 241172fa282SRobert Weinmeister svg: encodeURIComponent(mermaidSvg) 242172fa282SRobert Weinmeister }, 243172fa282SRobert Weinmeister function(response) { 244172fa282SRobert Weinmeister if(response.status == 'success') { 245172fa282SRobert Weinmeister location.reload(true); 246172fa282SRobert Weinmeister } 247172fa282SRobert Weinmeister else { 248172fa282SRobert Weinmeister alert(response.data[0]); 249172fa282SRobert Weinmeister } 250172fa282SRobert Weinmeister }, 251172fa282SRobert Weinmeister 'json' 252172fa282SRobert Weinmeister )}; 253172fa282SRobert Weinmeister 254172fa282SRobert Weinmeister jQuery('.mermaidlocked, .mermaid').each(function(index, element) { 2551da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseenter', function() { 256172fa282SRobert Weinmeister document.getElementById('mermaidFieldset' + index).style.display = 'flex'; 2571da12d6eSRobert Weinmeister }); 2581da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseleave', function() { 259172fa282SRobert Weinmeister document.getElementById('mermaidFieldset' + index).style.display = 'none'; 2601da12d6eSRobert Weinmeister }); 2611da12d6eSRobert Weinmeister 262172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaidlocked')) { 263172fa282SRobert Weinmeister document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 2641da12d6eSRobert Weinmeister var svgContent = element.innerHTML.trim(); 2651da12d6eSRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 2661da12d6eSRobert Weinmeister var link = document.createElement('a'); 2671da12d6eSRobert Weinmeister link.href = URL.createObjectURL(blob); 2681da12d6eSRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 2691da12d6eSRobert Weinmeister link.click(); 2701da12d6eSRobert Weinmeister URL.revokeObjectURL(link.href); 2711da12d6eSRobert Weinmeister }); 272172fa282SRobert Weinmeister 273172fa282SRobert Weinmeister document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 274172fa282SRobert Weinmeister if(confirm('Unlock Mermaid diagram?')) { 275172fa282SRobert Weinmeister callDokuWikiPHP('unlock', index, originalMermaidContent, element.innerHTML.trim()); 276172fa282SRobert Weinmeister } 277172fa282SRobert Weinmeister }); 278172fa282SRobert Weinmeister } 279172fa282SRobert Weinmeister 280172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaid')) { 281172fa282SRobert Weinmeister var originalMermaidContent = element.innerHTML; 282172fa282SRobert Weinmeister var observer = new MutationObserver(function(mutations) { 283172fa282SRobert Weinmeister mutations.forEach(function(mutation) { 284172fa282SRobert Weinmeister if (mutation.type === 'childList' && element.innerHTML.startsWith('<svg')) { 285172fa282SRobert Weinmeister document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 286172fa282SRobert Weinmeister var svgContent = element.innerHTML.trim(); 287172fa282SRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 288172fa282SRobert Weinmeister var link = document.createElement('a'); 289172fa282SRobert Weinmeister link.href = URL.createObjectURL(blob); 290172fa282SRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 291172fa282SRobert Weinmeister link.click(); 292172fa282SRobert Weinmeister URL.revokeObjectURL(link.href); 293172fa282SRobert Weinmeister }); 294172fa282SRobert Weinmeister 295172fa282SRobert Weinmeister document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 296172fa282SRobert Weinmeister if(confirm('Lock Mermaid diagram? [experimental]')) { 297172fa282SRobert Weinmeister callDokuWikiPHP('lock', index, originalMermaidContent, element.innerHTML.trim()); 298172fa282SRobert Weinmeister } 299172fa282SRobert Weinmeister }); 3001da12d6eSRobert Weinmeister } 3011da12d6eSRobert Weinmeister }); 3021da12d6eSRobert Weinmeister }); 3031da12d6eSRobert Weinmeister observer.observe(element, config); 304172fa282SRobert Weinmeister } 3051da12d6eSRobert Weinmeister }); 3061da12d6eSRobert Weinmeister});" 3071da12d6eSRobert Weinmeister ); 30846a60b4fSRobertWeinmeister } 30946a60b4fSRobertWeinmeister} 3101da12d6eSRobert Weinmeister 311