1<?php 2 3/** 4 * DokuWiki Plugin Mermaid (Syntax Component) 5 * 6 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 7 * @author Robert Weinmeister <develop@weinmeister.org> 8 */ 9 10declare(strict_types=1); 11 12if (!defined('DOKU_INC')) 13{ 14 die(); 15} 16 17use dokuwiki\Parsing\Parser; 18 19class syntax_plugin_mermaid extends \dokuwiki\Extension\SyntaxPlugin 20{ 21 // Constants for DokuWiki link handling 22 private const DOKUWIKI_LINK_START_MERMAID = '<code>DOKUWIKILINKSTARTMERMAID</code>'; 23 private const DOKUWIKI_LINK_END_MERMAID = '<code>DOKUWIKILINKENDMERMAID</code>'; 24 private const DOKUWIKI_LINK_SPLITTER ='--'; 25 26 // SVG icons as constants 27 private 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>'; 28 private 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>'; 29 private 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>'; 30 31 private int $mermaidCounter = -1; 32 private string $mermaidContent = ''; 33 private bool $currentMermaidIsLocked = false; 34 private string $mermaidContentIfLocked = ''; 35 36 /** 37 * Processes Gantt chart links in Mermaid diagrams 38 * 39 * Converts DokuWiki internal and external links into Mermaid-compatible 40 * click events for Gantt chart tasks. 41 * 42 * @param array $instructions The parser instructions to process 43 * @return array Modified instructions with properly formatted Gantt links 44 */ 45 private function processGanttLinks($instructions): array { 46 $modified_instructions = $instructions; 47 48 for ($i = 0; $i < count($modified_instructions); $i++) 49 { 50 if (!in_array($modified_instructions[$i][0], ['externallink', 'internallink'])) { 51 continue; 52 } 53 54 // use the appropriate link 55 $link = $modified_instructions[$i][0] === "externallink" 56 ? $modified_instructions[$i][1][0] 57 : wl($modified_instructions[$i][1][0], '', true); 58 59 // change link here to just the name 60 $modified_instructions[$i][0] = "cdata"; 61 if($modified_instructions[$i][1][1] !== null) { 62 unset($modified_instructions[$i][1][0]); 63 } 64 65 // insert the click event 66 $click_reference = ''; 67 if (preg_match('/(?<=:\s)\S+(?=,)/', $modified_instructions[$i+1][1][0], $output_array)) { 68 $click_reference = $output_array[0]; 69 } 70 71 array_splice( 72 $modified_instructions, 73 $i + 2, 74 0, 75 [["cdata", ["\nclick {$click_reference} href \"{$link}\"\n"]]] 76 ); 77 78 // encode colons 79 if (isset($modified_instructions[$i][1][0]) && is_string($modified_instructions[$i][1][0])) { 80 $modified_instructions[$i][1][0] = str_replace(":", "#colon;", $modified_instructions[$i][1][0]); 81 } 82 } 83 84 return $modified_instructions; 85 } 86 87 /** 88 * Protects DokuWiki link brackets from being processed 89 * 90 * @param string $text Text containing DokuWiki links 91 * @return string Text with protected DokuWiki link brackets 92 */ 93 private function protectBracketsFromDokuWiki(string $text): string { 94 $splitText = explode(self::DOKUWIKI_LINK_SPLITTER, $text); 95 foreach ($splitText as $key => $line) { 96 $splitText[$key] = preg_replace( 97 '/(?<!["\[(\s])(\[\[)(.*)(\]\])/', 98 self::DOKUWIKI_LINK_START_MERMAID . '$2' . self::DOKUWIKI_LINK_END_MERMAID, 99 $line 100 ) ?? $line; 101 } 102 return implode(self::DOKUWIKI_LINK_SPLITTER, $splitText); 103 } 104 105 /** 106 * Reverts the protection of DokuWiki link brackets 107 * 108 * @param string $text Text containing protected DokuWiki links 109 * @return string Text with restored DokuWiki link brackets 110 */ 111 private function removeProtectionOfBracketsFromDokuWiki(string $text): string { 112 return str_replace( 113 [self::DOKUWIKI_LINK_START_MERMAID, self::DOKUWIKI_LINK_END_MERMAID], 114 ['[[', ']]'], 115 $text 116 ); 117 } 118 119 /** @inheritDoc */ 120 function getType(): string { 121 return 'container'; 122 } 123 124 /** @inheritDoc */ 125 function getSort(): int { 126 return 150; 127 } 128 129 /** 130 * Entry pattern for Mermaid 131 * 132 * @param string $mode Parser mode 133 * @return void 134 */ 135 public function connectTo($mode): void { 136 $this->Lexer->addEntryPattern( 137 '<mermaid.*?>(?=.*?</mermaid>)', 138 $mode, 139 'plugin_mermaid'); 140 } 141 142 /** 143 * Exit pattern for Mermaid 144 * 145 * @return void 146 */ 147 function postConnect(): void { 148 $this->Lexer->addExitPattern( 149 '</mermaid>', 150 'plugin_mermaid' 151 ); 152 } 153 154 /** 155 * Handles the matched text based on the lexer state 156 * 157 * @param string $match Matched text from the lexer 158 * @param int $state Current lexer state (DOKU_LEXER_ENTER, DOKU_LEXER_UNMATCHED, DOKU_LEXER_EXIT) 159 * @param Doku_Handler $handler DokuWiki handler instance 160 * @return array Array with the state and processed match 161 */ 162 function handle($match, $state, $pos, Doku_Handler $handler): array { 163 return [$state, $match]; 164 } 165 166 /** 167 * Render xhtml output or metadata 168 */ 169 function render($mode, Doku_Renderer $renderer, $indata) 170 { 171 if($mode == 'xhtml'){ 172 list($state, $match) = $indata; 173 switch ($state) { 174 case DOKU_LEXER_ENTER: 175 $this->mermaidCounter++; 176 $values = explode(" ", $match); 177 $divwidth = count($values) < 2 ? 'auto' : $values[1]; 178 $divheight = count($values) < 3 ? 'auto' : substr($values[2], 0, -1); 179 $this->mermaidContent .= '<div id="mermaidContainer'.$this->mermaidCounter.'" style="position: relative; width:'.$divwidth.'; height:'.$divheight.'">'; 180 $this->mermaidContentIfLocked = $this->mermaidContent . '<span class="mermaidlocked" id=mermaidContent'.$this->mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">'; 181 $this->mermaidContent .= '<span class="mermaid" id=mermaidContent'.$this->mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">'; 182 break; 183 case DOKU_LEXER_UNMATCHED: 184 $explodedMatch = explode("\n", $match); 185 186 if(str_starts_with($explodedMatch[1], '%%<svg')) 187 { 188 $this->currentMermaidIsLocked = true; 189 $this->mermaidContent = $this->mermaidContentIfLocked . substr($explodedMatch[1], 2); 190 break; 191 } 192 else 193 { 194 $this->currentMermaidIsLocked = false; 195 } 196 197 $israwmode = isset($explodedMatch[1]) && strpos($explodedMatch[1], 'raw') !== false; 198 if($israwmode) 199 { 200 array_shift($explodedMatch); 201 array_shift($explodedMatch); 202 $actualContent = implode("\n", $explodedMatch); 203 $this->mermaidContent .= $actualContent; 204 } 205 else 206 { 207 $instructions = $this->p_get_instructions($this->protectBracketsFromDokuWiki($match)); 208 if (strpos($instructions[2][1][0], "gantt")) 209 { 210 $instructions = $this->processGanttLinks($instructions); 211 } 212 $xhtml = $this->removeProtectionOfBracketsFromDokuWiki($this->p_render($instructions)); 213 $this->mermaidContent .= preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $xhtml); 214 } 215 break; 216 case DOKU_LEXER_EXIT: 217 $this->mermaidContent .= "\r\n</span>"; 218 if($this->getConf('showSaveButton') or $this->getConf('showLockButton')) 219 { 220 $this->mermaidContent .= '<fieldset id="mermaidFieldset'.$this->mermaidCounter.'" style="position: absolute; top: 0; left: 0; display: none; width:auto; border: none">'; 221 if($this->getConf('showSaveButton')) 222 { 223 $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>'; 224 } 225 if($this->getConf('showLockButton')) 226 { 227 $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>'; 228 } 229 $this->mermaidContent .= '</fieldset>'; 230 } 231 $this->mermaidContent .= '</div>'; 232 233 $renderer->doc .= $this->mermaidContent; 234 $this->mermaidContent = ''; 235 break; 236 } 237 return true; 238 } 239 return false; 240 } 241 242 /* 243 * Get the parser instructions suitable for the mermaid 244 * 245 */ 246 function p_get_instructions($text) 247 { 248 //import parser classes and mode definitions 249 require_once DOKU_INC . 'inc/parser/parser.php'; 250 251 // https://www.dokuwiki.org/devel:parser 252 // https://www.dokuwiki.org/devel:parser#basic_invocation 253 // Create the parser and the handler 254 $Parser = new Parser(new Doku_Handler()); 255 256 $modes = array(); 257 258 // add default modes 259 $std_modes = array( 'internallink', 'media', 'externallink'); 260 261 foreach($std_modes as $m) 262 { 263 $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m); 264 $obj = new $class(); 265 $modes[] = array( 266 'sort' => $obj->getSort(), 267 'mode' => $m, 268 'obj' => $obj 269 ); 270 } 271 272 // add formatting modes 273 $fmt_modes = array( 'strong', 'emphasis', 'underline', 'monospace', 'subscript', 'superscript', 'deleted'); 274 foreach($fmt_modes as $m) 275 { 276 $obj = new \dokuwiki\Parsing\ParserMode\Formatting($m); 277 $modes[] = array( 278 'sort' => $obj->getSort(), 279 'mode' => $m, 280 'obj' => $obj 281 ); 282 } 283 284 //add modes to parser 285 foreach($modes as $mode) 286 { 287 $Parser->addMode($mode['mode'],$mode['obj']); 288 } 289 290 // Do the parsing 291 $p = $Parser->parse($text); 292 293 return $p; 294 } 295 296 public function p_render($instructions) 297 { 298 299 $Renderer = p_get_renderer('mermaid'); 300 301 // Loop through the instructions 302 foreach ($instructions as $instruction) { 303 if(method_exists($Renderer, $instruction[0])){ 304 call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array()); 305 } 306 } 307 return $Renderer->doc; 308 } 309}