1<?php 2/** 3 * DokuWiki Plugin mermaid (Action Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Robert Weinmeister <develop@weinmeister.org> 7 */ 8 9if (!defined('DOKU_INC')) die(); 10 11class action_plugin_mermaid extends \dokuwiki\Extension\ActionPlugin 12{ 13 /** @inheritDoc */ 14 public function register(Doku_Event_Handler $controller) 15 { 16 $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load'); 17 $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxRequest'); 18 } 19 20 public function handleAjaxRequest(Doku_Event $event, $param) { 21 if ($event->data !== 'plugin_mermaid') 22 { 23 return; 24 } 25 26 $event->stopPropagation(); 27 $event->preventDefault(); 28 29 $ID = cleanID(urldecode($_REQUEST['pageid'])); 30 31 if(auth_quickaclcheck($ID) < AUTH_EDIT) 32 { 33 echo json_encode(['status' => 'failure', 'data' => ['You do not have permission to edit this file.\nAccess was denied.']]); 34 exit(); 35 } 36 37 if(checklock($ID)) 38 { 39 echo json_encode(['status' => 'failure', 'data' => ['The page is currently locked.\nTry again later.']]); 40 exit(); 41 } 42 43 $wikitext = rawWiki($ID); 44 $newWikitext = $wikitext; 45 46 if($_REQUEST['mode'] == 'lock') 47 { 48 preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE); 49 50 if(is_array($matches) && count($matches[0]) > $_REQUEST['mermaidindex']) 51 { 52 $whereToInsert = $matches[1][$_REQUEST['mermaidindex']][1]; 53 $newWikitext = substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert); 54 } 55 else 56 { 57 echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]); 58 exit(); 59 } 60 } 61 62 if($_REQUEST['mode'] == 'unlock') 63 { 64 $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count); 65 if($count != 1) 66 { 67 echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]); 68 exit(); 69 } 70 } 71 72 if(strlen($newWikitext) > 0 && $newWikitext != $wikitext) 73 { 74 lock($ID); 75 saveWikiText($ID, $newWikitext, $_REQUEST['mode'] . ' Mermaid diagram', $minoredit = true); 76 unlock($ID); 77 78 echo json_encode(['status' => 'success', 'data' => []]); 79 exit(); 80 } 81 82 echo json_encode(['status' => 'failure', 'data' => ['Could not '.$_REQUEST['mode'].' the Mermaid diagram.']]); 83 exit(); 84 } 85 86 public function load(Doku_Event $event, $param) 87 { 88 // only load mermaid if it is needed 89 if(strpos(rawWiki(getID()), '<mermaid') === false) 90 { 91 return; 92 } 93 94 // Can be changed for debugging Mermaid 95 // https://mermaid.js.org/config/directives.html#changing-loglevel-via-directive 96 define("MERMAIDLOGLEVEL", "error"); 97 98 $theme = $this->getConf('theme'); 99 $init = "mermaid.initialize({startOnLoad: true, logLevel: '".MERMAIDLOGLEVEL."', theme: '".$theme."'});"; 100 $location = $this->getConf('location'); 101 102 switch ($location) { 103 case 'local': 104 $event->data['script'][] = array 105 ( 106 'type' => 'text/javascript', 107 'charset' => 'utf-8', 108 'src' => DOKU_BASE.'lib/plugins/mermaid/mermaid.min.js' 109 ); 110 break; 111 case 'latest': 112 case 'remote1091': 113 // options remote108, remote106, remote104, remote103, remote102, remote101, remote100 are depreciated and only included for backward compatibility 114 case 'remote108': 115 case 'remote106': 116 case 'remote104': 117 case 'remote103': 118 case 'remote102': 119 case 'remote101': 120 case 'remote100': 121 $versions = array( 122 'latest' => '', 123 'remote1091' => '@10.9.1', 124 'remote108' => '@10.8.0', 125 'remote106' => '@10.6.1', 126 'remote104' => '@10.4.0', 127 'remote103' => '@10.3.1', 128 'remote102' => '@10.2.4', 129 'remote101' => '@10.1.0', 130 'remote100' => '@10.0.2' 131 ); 132 $data = "import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid".$versions[$location]."/dist/mermaid.esm.min.mjs';".$init; 133 $event->data['script'][] = array 134 ( 135 'type' => 'module', 136 'charset' => 'utf-8', 137 '_data' => $data 138 ); 139 break; 140 // option remote94 is depreciated and only included for backward compatibility 141 case 'remote94': 142 case 'remote943': 143 $event->data['script'][] = array 144 ( 145 'type' => 'text/javascript', 146 'charset' => 'utf-8', 147 'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.4.3/dist/mermaid.min.js' 148 ); 149 break; 150 // option remote93 is depreciated and only included for backward compatibility 151 case 'remote93': 152 $event->data['script'][] = array 153 ( 154 'type' => 'text/javascript', 155 'charset' => 'utf-8', 156 'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.3.0/dist/mermaid.min.js' 157 ); 158 break; 159 default: 160 } 161 162 $event->data['link'][] = array 163 ( 164 'rel' => 'stylesheet', 165 'type' => 'text/css', 166 'href' => DOKU_BASE."lib/plugins/mermaid/mermaid.css", 167 ); 168 169 switch ($location) { 170 case 'local': 171 case 'remote943': 172 // options remote94 and remote93 are depreciated and only included for backward compatibility 173 case 'remote94': 174 case 'remote93': 175 $event->data['script'][] = array 176 ( 177 'type' => 'text/javascript', 178 'charset' => 'utf-8', 179 '_data' => $init 180 ); 181 break; 182 default: 183 } 184 185 // remove the search highlight from DokuWiki as it interferes with the Mermaid parsing/rendering 186 $event->data['script'][] = array 187 ( 188 'type' => 'text/javascript', 189 'charset' => 'utf-8', 190 '_data' => "document.addEventListener('DOMContentLoaded', function() { 191 jQuery('.mermaid').each(function() { 192 var modifiedContent = jQuery(this).html().replace(/<span class=\"search_hit\">(.+?)<\/span>/g, '$1'); 193 jQuery(this).html(modifiedContent); 194 }) 195 });" 196 ); 197 198 // adds image-save capability 199 // First: Wait until the DOM content is fully loaded 200 // Second: Wait until Mermaid has changed the dokuwiki content to an svg 201 $event->data['script'][] = array 202 ( 203 'type' => 'text/javascript', 204 'charset' => 'utf-8', 205 '_data' => " 206document.addEventListener('DOMContentLoaded', function() { 207 var config = { 208 childList: true, 209 subtree: true, 210 characterData: true 211 }; 212 213 function callDokuWikiPHP(mode, index, mermaidRaw, mermaidSvg) { 214 jQuery.post( 215 DOKU_BASE + 'lib/exe/ajax.php', 216 { 217 call: 'plugin_mermaid', 218 mode: mode, 219 mermaidindex: index, 220 pageid: '".getID()."', 221 svg: encodeURIComponent(mermaidSvg) 222 }, 223 function(response) { 224 if(response.status == 'success') { 225 location.reload(true); 226 } 227 else { 228 alert(response.data[0]); 229 } 230 }, 231 'json' 232 )}; 233 234 jQuery('.mermaidlocked, .mermaid').each(function(index, element) { 235 document.getElementById('mermaidContainer' + index).addEventListener('mouseenter', function() { 236 document.getElementById('mermaidFieldset' + index).style.display = 'flex'; 237 }); 238 document.getElementById('mermaidContainer' + index).addEventListener('mouseleave', function() { 239 document.getElementById('mermaidFieldset' + index).style.display = 'none'; 240 }); 241 242 if(jQuery(element).hasClass('mermaidlocked')) { 243 document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 244 var svgContent = element.innerHTML.trim(); 245 var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 246 var link = document.createElement('a'); 247 link.href = URL.createObjectURL(blob); 248 link.download = 'mermaid' + index + '.svg'; 249 link.click(); 250 URL.revokeObjectURL(link.href); 251 }); 252 253 document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 254 if(confirm('Unlock Mermaid diagram?')) { 255 callDokuWikiPHP('unlock', index, originalMermaidContent, element.innerHTML.trim()); 256 } 257 }); 258 } 259 260 if(jQuery(element).hasClass('mermaid')) { 261 var originalMermaidContent = element.innerHTML; 262 var observer = new MutationObserver(function(mutations) { 263 mutations.forEach(function(mutation) { 264 if (mutation.type === 'childList' && element.innerHTML.startsWith('<svg')) { 265 document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => { 266 var svgContent = element.innerHTML.trim(); 267 var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 268 var link = document.createElement('a'); 269 link.href = URL.createObjectURL(blob); 270 link.download = 'mermaid' + index + '.svg'; 271 link.click(); 272 URL.revokeObjectURL(link.href); 273 }); 274 275 document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => { 276 if(confirm('Lock Mermaid diagram? [experimental]')) { 277 callDokuWikiPHP('lock', index, originalMermaidContent, element.innerHTML.trim()); 278 } 279 }); 280 } 281 }); 282 }); 283 observer.observe(element, config); 284 } 285 }); 286});" 287 ); 288 } 289} 290 291