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