1<?php 2/** 3 * DokuWiki Plugin mermaid (Syntax Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Robert Weinmeister <develop@weinmeister.org> 7 */ 8 9use dokuwiki\Parsing\Parser; 10 11class syntax_plugin_mermaid extends \dokuwiki\Extension\SyntaxPlugin 12{ 13 const DOKUWIKI_LINK_START_MERMAID = '<code>DOKUWIKILINKSTARTMERMAID</code>'; 14 const DOKUWIKI_LINK_END_MERMAID = '<code>DOKUWIKILINKENDMERMAID</code>'; 15 const DOKUWIKI_LINK_SPLITTER ='--'; 16 const DOKUWIKI_SVG_SAVE = '<svg fill="#000000" viewBox="0 0 52 52" enable-background="new 0 0 52 52" xml:space="preserve" style="width: 24px; height: 24px;"><path d="M37.1,4v13.6c0,1-0.8,1.9-1.9,1.9H13.9c-1,0-1.9-0.8-1.9-1.9V4H8C5.8,4,4,5.8,4,8v36c0,2.2,1.8,4,4,4h36 c2.2,0,4-1.8,4-4V11.2L40.8,4H37.1z M44.1,42.1c0,1-0.8,1.9-1.9,1.9H9.9c-1,0-1.9-0.8-1.9-1.9V25.4c0-1,0.8-1.9,1.9-1.9h32.3 c1,0,1.9,0.8,1.9,1.9V42.1z"/><path d="M24.8,13.6c0,1,0.8,1.9,1.9,1.9h4.6c1,0,1.9-0.8,1.9-1.9V4h-8.3L24.8,13.6L24.8,13.6z"/></svg>'; 17 const DOKUWIKI_SVG_LOCKED = '<svg viewBox="0 0 16 16" fill="none" style="width: 24px; height: 24px;"><path fill-rule="evenodd" clip-rule="evenodd" d="M4 6V4C4 1.79086 5.79086 0 8 0C10.2091 0 12 1.79086 12 4V6H14V16H2V6H4ZM6 4C6 2.89543 6.89543 2 8 2C9.10457 2 10 2.89543 10 4V6H6V4ZM7 13V9H9V13H7Z" fill="#000000"/></svg>'; 18 const DOKUWIKI_SVG_UNLOCKED = '<svg style="width: 24px; height: 24px;" viewBox="0 0 16 16" fill="none"><path fill-rule="evenodd" clip-rule="evenodd" d="M11.5 2C10.6716 2 10 2.67157 10 3.5V6H13V16H1V6H8V3.5C8 1.567 9.567 0 11.5 0C13.433 0 15 1.567 15 3.5V4H13V3.5C13 2.67157 12.3284 2 11.5 2ZM9 10H5V12H9V10Z" fill="#000000"/></svg>'; 19 20 private $mermaidCounter = -1; 21 private $mermaidContent; 22 private $currentMermaidIsLocked = false; 23 private $mermaidContentIfLocked; 24 25 function enable_gantt_links($instructions) 26 { 27 $modified_instructions = $instructions; 28 29 for ($i = 0; $i < count($modified_instructions); $i++) 30 { 31 if (in_array($modified_instructions[$i][0], ["externallink", "internallink"])) 32 { 33 // use the appropriate link 34 $link = $modified_instructions[$i][0] == "externallink" ? $modified_instructions[$i][1][0] : wl($modified_instructions[$i][1][0], '', true); 35 36 // change link here to just the name of the link 37 $modified_instructions[$i][0]= "cdata"; 38 if(!is_null($modified_instructions[$i][1][1])) 39 { 40 unset($modified_instructions[$i][1][0]); 41 } 42 43 // insert the click event 44 if (preg_match('/(?<=:\s)\S+(?=,)/', $modified_instructions[$i+1][1][0], $output_array)) 45 { 46 $click_reference = $output_array[0]; 47 } 48 array_splice($modified_instructions, $i + 2, 0, [["cdata", ["\nclick ".$click_reference." href \"".$link."\"\n"]]]); 49 50 // encode colons 51 $modified_instructions[$i][1][0] = str_replace(":", "#colon;", $modified_instructions[$i][1][0]); 52 } 53 } 54 55 return $modified_instructions; 56 } 57 58 function protect_brackets_from_dokuwiki($text) 59 { 60 $splitText = explode(self::DOKUWIKI_LINK_SPLITTER, $text); 61 foreach ($splitText as $key => $line) 62 { 63 $splitText[$key] = preg_replace('/(?<!["\[(\s])(\[\[)(.*)(\]\])/', self::DOKUWIKI_LINK_START_MERMAID . '$2' . self::DOKUWIKI_LINK_END_MERMAID, $line); 64 } 65 $text = implode(self::DOKUWIKI_LINK_SPLITTER, $splitText); 66 return $text; 67 } 68 69 function remove_protection_of_brackets_from_dokuwiki($text) 70 { 71 return str_replace(self::DOKUWIKI_LINK_START_MERMAID, '[[', str_replace(self::DOKUWIKI_LINK_END_MERMAID, ']]', $text)); 72 } 73 74 /** @inheritDoc */ 75 function getType() 76 { 77 return 'container'; 78 } 79 80 /** @inheritDoc */ 81 function getSort() 82 { 83 return 150; 84 } 85 86 /** 87 * Connect lookup pattern to lexer. 88 * 89 * @param string $mode Parser mode 90 */ 91 function connectTo($mode) 92 { 93 $this->Lexer->addEntryPattern('<mermaid.*?>(?=.*?</mermaid>)',$mode,'plugin_mermaid'); 94 } 95 96 function postConnect() 97 { 98 $this->Lexer->addExitPattern('</mermaid>','plugin_mermaid'); 99 } 100 101 /** 102 * Handle matches of the Mermaid syntax 103 */ 104 function handle($match, $state, $pos, Doku_Handler $handler) 105 { 106 switch ($state) 107 { 108 case DOKU_LEXER_ENTER: 109 return array($state, $match); 110 case DOKU_LEXER_UNMATCHED: 111 return array($state, $match); 112 case DOKU_LEXER_EXIT: 113 return array($state, ''); 114 } 115 return false; 116 } 117 118 /** 119 * Render xhtml output or metadata 120 */ 121 function render($mode, Doku_Renderer $renderer, $indata) 122 { 123 if($mode == 'xhtml'){ 124 list($state, $match) = $indata; 125 switch ($state) { 126 case DOKU_LEXER_ENTER: 127 $this->mermaidCounter++; 128 $values = explode(" ", $match); 129 $divwidth = count($values) < 2 ? 'auto' : $values[1]; 130 $divheight = count($values) < 3 ? 'auto' : substr($values[2], 0, -1); 131 $this->mermaidContent .= '<div id="mermaidContainer'.$this->mermaidCounter.'" style="position: relative; width:'.$divwidth.'; height:'.$divheight.'">'; 132 $this->mermaidContentIfLocked = $this->mermaidContent . '<span class="mermaidlocked" id=mermaidContent'.$this->mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">'; 133 $this->mermaidContent .= '<span class="mermaid" id=mermaidContent'.$this->mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">'; 134 break; 135 case DOKU_LEXER_UNMATCHED: 136 $explodedMatch = explode("\n", $match); 137 138 if(str_starts_with($explodedMatch[1], '%%<svg')) 139 { 140 $this->currentMermaidIsLocked = true; 141 $this->mermaidContent = $this->mermaidContentIfLocked . substr($explodedMatch[1], 2); 142 break; 143 } 144 else 145 { 146 $this->currentMermaidIsLocked = false; 147 } 148 149 $israwmode = isset($explodedMatch[1]) && strpos($explodedMatch[1], 'raw') !== false; 150 if($israwmode) 151 { 152 array_shift($explodedMatch); 153 array_shift($explodedMatch); 154 $actualContent = implode("\n", $explodedMatch); 155 $this->mermaidContent .= $actualContent; 156 } 157 else 158 { 159 $instructions = $this->p_get_instructions($this->protect_brackets_from_dokuwiki($match)); 160 if (strpos($instructions[2][1][0], "gantt")) 161 { 162 $instructions = $this->enable_gantt_links($instructions); 163 } 164 $xhtml = $this->remove_protection_of_brackets_from_dokuwiki($this->p_render($instructions)); 165 $this->mermaidContent .= preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $xhtml); 166 } 167 break; 168 case DOKU_LEXER_EXIT: 169 $this->mermaidContent .= "\r\n</span>"; 170 $this->mermaidContent .= '<fieldset id="mermaidFieldset'.$this->mermaidCounter.'" style="position: absolute; top: 0; left: 0; display: none; width:auto; border: none">'; 171 $this->mermaidContent .= '<button id="mermaidButtonSave'.$this->mermaidCounter.'" style="z-index: 10; display: block; padding: 0; margin: 0; border: none; background: none; width: 24px; height: 24px;">'.self::DOKUWIKI_SVG_SAVE.'</button>'; 172 $this->mermaidContent .= '<button id="mermaidButtonPermanent'.$this->mermaidCounter.'" style="z-index: 10; display: block; padding: 0; margin: 0; border: none; background: none; width: 24px; height: 24px;">'.($this->currentMermaidIsLocked ? self::DOKUWIKI_SVG_LOCKED : self::DOKUWIKI_SVG_UNLOCKED).'</button>'; 173 $this->mermaidContent .= '</fieldset></div>'; 174 175 $renderer->doc .= $this->mermaidContent; 176 $this->mermaidContent = ''; 177 break; 178 } 179 return true; 180 } 181 return false; 182 } 183 184 /* 185 * Get the parser instructions suitable for the mermaid 186 * 187 */ 188 function p_get_instructions($text) 189 { 190 //import parser classes and mode definitions 191 require_once DOKU_INC . 'inc/parser/parser.php'; 192 193 // https://www.dokuwiki.org/devel:parser 194 // https://www.dokuwiki.org/devel:parser#basic_invocation 195 // Create the parser and the handler 196 $Parser = new Parser(new Doku_Handler()); 197 198 $modes = array(); 199 200 // add default modes 201 $std_modes = array( 'internallink', 'media', 'externallink'); 202 203 foreach($std_modes as $m) 204 { 205 $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m); 206 $obj = new $class(); 207 $modes[] = array( 208 'sort' => $obj->getSort(), 209 'mode' => $m, 210 'obj' => $obj 211 ); 212 } 213 214 // add formatting modes 215 $fmt_modes = array( 'strong', 'emphasis', 'underline', 'monospace', 'subscript', 'superscript', 'deleted'); 216 foreach($fmt_modes as $m) 217 { 218 $obj = new \dokuwiki\Parsing\ParserMode\Formatting($m); 219 $modes[] = array( 220 'sort' => $obj->getSort(), 221 'mode' => $m, 222 'obj' => $obj 223 ); 224 } 225 226 //add modes to parser 227 foreach($modes as $mode) 228 { 229 $Parser->addMode($mode['mode'],$mode['obj']); 230 } 231 232 // Do the parsing 233 $p = $Parser->parse($text); 234 235 return $p; 236 } 237 238 public function p_render($instructions) 239 { 240 $Renderer = p_get_renderer('mermaid'); 241 242 // Loop through the instructions 243 foreach ($instructions as $instruction) { 244 if(method_exists($Renderer, $instruction[0])){ 245 call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array()); 246 } 247 } 248 249 return $Renderer->doc; 250 } 251}