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