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 946a60b4fSRobertWeinmeisterclass action_plugin_mermaid extends \dokuwiki\Extension\ActionPlugin 1046a60b4fSRobertWeinmeister{ 1146a60b4fSRobertWeinmeister /** @inheritDoc */ 1246a60b4fSRobertWeinmeister public function register(Doku_Event_Handler $controller) 1346a60b4fSRobertWeinmeister { 1446a60b4fSRobertWeinmeister $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load'); 15*172fa282SRobert Weinmeister $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxRequest'); 16*172fa282SRobert Weinmeister } 17*172fa282SRobert Weinmeister 18*172fa282SRobert Weinmeister public function handleAjaxRequest(Doku_Event $event, $param) { 19*172fa282SRobert Weinmeister if ($event->data !== 'plugin_mermaid') 20*172fa282SRobert Weinmeister { 21*172fa282SRobert Weinmeister return; 22*172fa282SRobert Weinmeister } 23*172fa282SRobert Weinmeister 24*172fa282SRobert Weinmeister $event->stopPropagation(); 25*172fa282SRobert Weinmeister $event->preventDefault(); 26*172fa282SRobert Weinmeister 27*172fa282SRobert Weinmeister $ID = cleanID(urldecode($_REQUEST['pageid'])); 28*172fa282SRobert Weinmeister 29*172fa282SRobert Weinmeister if(auth_quickaclcheck($ID) < AUTH_EDIT) 30*172fa282SRobert Weinmeister { 31*172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['You do not have permission to edit this file.\nAccess was denied.']]); 32*172fa282SRobert Weinmeister exit(); 33*172fa282SRobert Weinmeister } 34*172fa282SRobert Weinmeister 35*172fa282SRobert Weinmeister if(checklock($ID)) 36*172fa282SRobert Weinmeister { 37*172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['The page is currently locked.\nTry again later.']]); 38*172fa282SRobert Weinmeister exit(); 39*172fa282SRobert Weinmeister } 40*172fa282SRobert Weinmeister 41*172fa282SRobert Weinmeister $wikitext = rawWiki($ID); 42*172fa282SRobert Weinmeister $newWikitext = $wikitext; 43*172fa282SRobert Weinmeister 44*172fa282SRobert Weinmeister if($_REQUEST['mode'] == 'lock') 45*172fa282SRobert Weinmeister { 46*172fa282SRobert Weinmeister preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE); 47*172fa282SRobert Weinmeister 48*172fa282SRobert Weinmeister if(is_array($matches) && count($matches[0]) > $_REQUEST['mermaidindex']) 49*172fa282SRobert Weinmeister { 50*172fa282SRobert Weinmeister $whereToInsert = $matches[1][$_REQUEST['mermaidindex']][1]; 51*172fa282SRobert Weinmeister $newWikitext = substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert); 52*172fa282SRobert Weinmeister } 53*172fa282SRobert Weinmeister else 54*172fa282SRobert Weinmeister { 55*172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]); 56*172fa282SRobert Weinmeister exit(); 57*172fa282SRobert Weinmeister } 58*172fa282SRobert Weinmeister } 59*172fa282SRobert Weinmeister 60*172fa282SRobert Weinmeister if($_REQUEST['mode'] == 'unlock') 61*172fa282SRobert Weinmeister { 62*172fa282SRobert Weinmeister $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count); 63*172fa282SRobert Weinmeister if($count != 1) 64*172fa282SRobert Weinmeister { 65*172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]); 66*172fa282SRobert Weinmeister exit(); 67*172fa282SRobert Weinmeister } 68*172fa282SRobert Weinmeister } 69*172fa282SRobert Weinmeister 70*172fa282SRobert Weinmeister if(strlen($newWikitext) > 0 && $newWikitext != $wikitext) 71*172fa282SRobert Weinmeister { 72*172fa282SRobert Weinmeister lock($ID); 73*172fa282SRobert Weinmeister saveWikiText($ID, $newWikitext, $_REQUEST['mode'] . ' Mermaid diagram', $minoredit = true); 74*172fa282SRobert Weinmeister unlock($ID); 75*172fa282SRobert Weinmeister 76*172fa282SRobert Weinmeister echo json_encode(['status' => 'success', 'data' => []]); 77*172fa282SRobert Weinmeister exit(); 78*172fa282SRobert Weinmeister } 79*172fa282SRobert Weinmeister 80*172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not '.$_REQUEST['mode'].' the Mermaid diagram.']]); 81*172fa282SRobert Weinmeister exit(); 8246a60b4fSRobertWeinmeister } 8346a60b4fSRobertWeinmeister 8446a60b4fSRobertWeinmeister public function load(Doku_Event $event, $param) 8546a60b4fSRobertWeinmeister { 866e5341c6SRobert Weinmeister // Can be changed for debugging Mermaid 876e5341c6SRobert Weinmeister // https://mermaid.js.org/config/directives.html#changing-loglevel-via-directive 886e5341c6SRobert Weinmeister define("MERMAIDLOGLEVEL", "error"); 896e5341c6SRobert Weinmeister 904c8bd9ffSRobert Weinmeister $theme = $this->getConf('theme'); 914c8bd9ffSRobert Weinmeister $init = "mermaid.initialize({startOnLoad: true, logLevel: '".MERMAIDLOGLEVEL."', theme: '".$theme."'});"; 926fcac025SRobert Weinmeister $location = $this->getConf('location'); 934c8bd9ffSRobert Weinmeister 946fcac025SRobert Weinmeister switch ($location) { 952d4b7fc2SRobert Weinmeister case 'local': 9646a60b4fSRobertWeinmeister $event->data['script'][] = array 9746a60b4fSRobertWeinmeister ( 9846a60b4fSRobertWeinmeister 'type' => 'text/javascript', 9946a60b4fSRobertWeinmeister 'charset' => 'utf-8', 1002d4b7fc2SRobert Weinmeister 'src' => DOKU_BASE.'lib/plugins/mermaid/mermaid.min.js' 10146a60b4fSRobertWeinmeister ); 1022d4b7fc2SRobert Weinmeister break; 1032d4b7fc2SRobert Weinmeister case 'latest': 104a788b843SRobert Weinmeister case 'remote1091': 105a788b843SRobert Weinmeister // options remote108, remote106, remote104, remote103, remote102, remote101, remote100 are depreciated and only included for backward compatibility 1066fcac025SRobert Weinmeister case 'remote108': 107a612c7d6SRobert Weinmeister case 'remote106': 1088eaa3f3bSRobert Weinmeister case 'remote104': 1097d8a2661SRobert Weinmeister case 'remote103': 1104df3d176SRobert Weinmeister case 'remote102': 1115f50b169SRobert Weinmeister case 'remote101': 1126e5341c6SRobert Weinmeister case 'remote100': 1136fcac025SRobert Weinmeister $versions = array( 1146fcac025SRobert Weinmeister 'latest' => '', 115a788b843SRobert Weinmeister 'remote1091' => '@10.9.1', 1166fcac025SRobert Weinmeister 'remote108' => '@10.8.0', 1176fcac025SRobert Weinmeister 'remote106' => '@10.6.1', 1186fcac025SRobert Weinmeister 'remote104' => '@10.4.0', 1196fcac025SRobert Weinmeister 'remote103' => '@10.3.1', 1206fcac025SRobert Weinmeister 'remote102' => '@10.2.4', 1216fcac025SRobert Weinmeister 'remote101' => '@10.1.0', 1226fcac025SRobert Weinmeister 'remote100' => '@10.0.2' 1236fcac025SRobert Weinmeister ); 1246fcac025SRobert Weinmeister $data = "import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid".$versions[$location]."/dist/mermaid.esm.min.mjs';".$init; 1256e5341c6SRobert Weinmeister $event->data['script'][] = array 1266e5341c6SRobert Weinmeister ( 1276e5341c6SRobert Weinmeister 'type' => 'module', 1286e5341c6SRobert Weinmeister 'charset' => 'utf-8', 1296fcac025SRobert Weinmeister '_data' => $data 1306e5341c6SRobert Weinmeister ); 1316e5341c6SRobert Weinmeister break; 132a788b843SRobert Weinmeister // option remote94 is depreciated and only included for backward compatibility 1336e5341c6SRobert Weinmeister case 'remote94': 134a788b843SRobert Weinmeister case 'remote943': 1356e5341c6SRobert Weinmeister $event->data['script'][] = array 1366e5341c6SRobert Weinmeister ( 1376e5341c6SRobert Weinmeister 'type' => 'text/javascript', 1386e5341c6SRobert Weinmeister 'charset' => 'utf-8', 1394df3d176SRobert Weinmeister 'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.4.3/dist/mermaid.min.js' 1406e5341c6SRobert Weinmeister ); 1416e5341c6SRobert Weinmeister break; 142a788b843SRobert Weinmeister // option remote93 is depreciated and only included for backward compatibility 1432d4b7fc2SRobert Weinmeister case 'remote93': 1442d4b7fc2SRobert Weinmeister $event->data['script'][] = array 1452d4b7fc2SRobert Weinmeister ( 1462d4b7fc2SRobert Weinmeister 'type' => 'text/javascript', 1472d4b7fc2SRobert Weinmeister 'charset' => 'utf-8', 1484df3d176SRobert Weinmeister 'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.3.0/dist/mermaid.min.js' 1492d4b7fc2SRobert Weinmeister ); 1502d4b7fc2SRobert Weinmeister break; 1512d4b7fc2SRobert Weinmeister default: 1522d4b7fc2SRobert Weinmeister } 15346a60b4fSRobertWeinmeister 15446a60b4fSRobertWeinmeister $event->data['link'][] = array 15546a60b4fSRobertWeinmeister ( 15646a60b4fSRobertWeinmeister 'rel' => 'stylesheet', 15746a60b4fSRobertWeinmeister 'type' => 'text/css', 15846a60b4fSRobertWeinmeister 'href' => DOKU_BASE."lib/plugins/mermaid/mermaid.css", 15946a60b4fSRobertWeinmeister ); 16046a60b4fSRobertWeinmeister 1616fcac025SRobert Weinmeister switch ($location) { 1624df3d176SRobert Weinmeister case 'local': 163a788b843SRobert Weinmeister case 'remote943': 164a788b843SRobert Weinmeister // options remote94 and remote93 are depreciated and only included for backward compatibility 1656e5341c6SRobert Weinmeister case 'remote94': 1666e5341c6SRobert Weinmeister case 'remote93': 16746a60b4fSRobertWeinmeister $event->data['script'][] = array 16846a60b4fSRobertWeinmeister ( 16946a60b4fSRobertWeinmeister 'type' => 'text/javascript', 17046a60b4fSRobertWeinmeister 'charset' => 'utf-8', 1714c8bd9ffSRobert Weinmeister '_data' => $init 17246a60b4fSRobertWeinmeister ); 1736e5341c6SRobert Weinmeister break; 1746e5341c6SRobert Weinmeister default: 1756e5341c6SRobert Weinmeister } 1766fcac025SRobert Weinmeister 1776fcac025SRobert Weinmeister // remove the search highlight from DokuWiki as it interferes with the Mermaid parsing/rendering 1786fcac025SRobert Weinmeister $event->data['script'][] = array 1796fcac025SRobert Weinmeister ( 1806fcac025SRobert Weinmeister 'type' => 'text/javascript', 1816fcac025SRobert Weinmeister 'charset' => 'utf-8', 1826fcac025SRobert Weinmeister '_data' => "document.addEventListener('DOMContentLoaded', function() { 1836fcac025SRobert Weinmeister jQuery('.mermaid').each(function() { 1846fcac025SRobert Weinmeister var modifiedContent = jQuery(this).html().replace(/<span class=\"search_hit\">(.+?)<\/span>/g, '$1'); 1856fcac025SRobert Weinmeister jQuery(this).html(modifiedContent); 1866fcac025SRobert Weinmeister }) 1876fcac025SRobert Weinmeister });" 1886fcac025SRobert Weinmeister ); 1891da12d6eSRobert Weinmeister 1901da12d6eSRobert Weinmeister // adds image-save capability 1911da12d6eSRobert Weinmeister // First: Wait until the DOM content is fully loaded 1921da12d6eSRobert Weinmeister // Second: Wait until Mermaid has changed the dokuwiki content to an svg 1931da12d6eSRobert Weinmeister $event->data['script'][] = array 1941da12d6eSRobert Weinmeister ( 1951da12d6eSRobert Weinmeister 'type' => 'text/javascript', 1961da12d6eSRobert Weinmeister 'charset' => 'utf-8', 1971da12d6eSRobert Weinmeister '_data' => " 1981da12d6eSRobert Weinmeisterdocument.addEventListener('DOMContentLoaded', function() { 1991da12d6eSRobert Weinmeister var config = { 2001da12d6eSRobert Weinmeister childList: true, 2011da12d6eSRobert Weinmeister subtree: true, 2021da12d6eSRobert Weinmeister characterData: true 2031da12d6eSRobert Weinmeister }; 2041da12d6eSRobert Weinmeister 205*172fa282SRobert Weinmeister function callDokuWikiPHP(mode, index, mermaidRaw, mermaidSvg) { 206*172fa282SRobert Weinmeister jQuery.post( 207*172fa282SRobert Weinmeister DOKU_BASE + 'lib/exe/ajax.php', 208*172fa282SRobert Weinmeister { 209*172fa282SRobert Weinmeister call: 'plugin_mermaid', 210*172fa282SRobert Weinmeister mode: mode, 211*172fa282SRobert Weinmeister mermaidindex: index, 212*172fa282SRobert Weinmeister pageid: '".getID()."', 213*172fa282SRobert Weinmeister svg: encodeURIComponent(mermaidSvg) 214*172fa282SRobert Weinmeister }, 215*172fa282SRobert Weinmeister function(response) { 216*172fa282SRobert Weinmeister if(response.status == 'success') { 217*172fa282SRobert Weinmeister location.reload(true); 218*172fa282SRobert Weinmeister } 219*172fa282SRobert Weinmeister else { 220*172fa282SRobert Weinmeister alert(response.data[0]); 221*172fa282SRobert Weinmeister } 222*172fa282SRobert Weinmeister }, 223*172fa282SRobert Weinmeister 'json' 224*172fa282SRobert Weinmeister )}; 225*172fa282SRobert Weinmeister 226*172fa282SRobert Weinmeister jQuery('.mermaidlocked, .mermaid').each(function(index, element) { 2271da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseenter', function() { 228*172fa282SRobert Weinmeister document.getElementById('mermaidFieldset' + index).style.display = 'flex'; 2291da12d6eSRobert Weinmeister }); 2301da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseleave', function() { 231*172fa282SRobert Weinmeister document.getElementById('mermaidFieldset' + index).style.display = 'none'; 2321da12d6eSRobert Weinmeister }); 2331da12d6eSRobert Weinmeister 234*172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaidlocked')) { 235*172fa282SRobert Weinmeister document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 2361da12d6eSRobert Weinmeister var svgContent = element.innerHTML.trim(); 2371da12d6eSRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 2381da12d6eSRobert Weinmeister var link = document.createElement('a'); 2391da12d6eSRobert Weinmeister link.href = URL.createObjectURL(blob); 2401da12d6eSRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 2411da12d6eSRobert Weinmeister link.click(); 2421da12d6eSRobert Weinmeister URL.revokeObjectURL(link.href); 2431da12d6eSRobert Weinmeister }); 244*172fa282SRobert Weinmeister 245*172fa282SRobert Weinmeister document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 246*172fa282SRobert Weinmeister if(confirm('Unlock Mermaid diagram?')) { 247*172fa282SRobert Weinmeister callDokuWikiPHP('unlock', index, originalMermaidContent, element.innerHTML.trim()); 248*172fa282SRobert Weinmeister } 249*172fa282SRobert Weinmeister }); 250*172fa282SRobert Weinmeister } 251*172fa282SRobert Weinmeister 252*172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaid')) { 253*172fa282SRobert Weinmeister var originalMermaidContent = element.innerHTML; 254*172fa282SRobert Weinmeister var observer = new MutationObserver(function(mutations) { 255*172fa282SRobert Weinmeister mutations.forEach(function(mutation) { 256*172fa282SRobert Weinmeister if (mutation.type === 'childList' && element.innerHTML.startsWith('<svg')) { 257*172fa282SRobert Weinmeister document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 258*172fa282SRobert Weinmeister var svgContent = element.innerHTML.trim(); 259*172fa282SRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 260*172fa282SRobert Weinmeister var link = document.createElement('a'); 261*172fa282SRobert Weinmeister link.href = URL.createObjectURL(blob); 262*172fa282SRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 263*172fa282SRobert Weinmeister link.click(); 264*172fa282SRobert Weinmeister URL.revokeObjectURL(link.href); 265*172fa282SRobert Weinmeister }); 266*172fa282SRobert Weinmeister 267*172fa282SRobert Weinmeister document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 268*172fa282SRobert Weinmeister if(confirm('Lock Mermaid diagram? [experimental]')) { 269*172fa282SRobert Weinmeister callDokuWikiPHP('lock', index, originalMermaidContent, element.innerHTML.trim()); 270*172fa282SRobert Weinmeister } 271*172fa282SRobert Weinmeister }); 2721da12d6eSRobert Weinmeister } 2731da12d6eSRobert Weinmeister }); 2741da12d6eSRobert Weinmeister }); 2751da12d6eSRobert Weinmeister observer.observe(element, config); 276*172fa282SRobert Weinmeister } 2771da12d6eSRobert Weinmeister }); 2781da12d6eSRobert Weinmeister});" 2791da12d6eSRobert Weinmeister ); 28046a60b4fSRobertWeinmeister } 28146a60b4fSRobertWeinmeister} 2821da12d6eSRobert Weinmeister 283