*/ declare(strict_types=1); if (!defined('DOKU_INC')) { die(); } use dokuwiki\Parsing\Parser; class syntax_plugin_mermaid extends \dokuwiki\Extension\SyntaxPlugin { // Constants for DokuWiki link handling private const DOKUWIKI_LINK_START_MERMAID = 'DOKUWIKILINKSTARTMERMAID'; private const DOKUWIKI_LINK_END_MERMAID = 'DOKUWIKILINKENDMERMAID'; private const DOKUWIKI_LINK_SPLITTER ='--'; // SVG icons as constants private const DOKUWIKI_SVG_SAVE = ''; private const DOKUWIKI_SVG_LOCKED = ''; private const DOKUWIKI_SVG_UNLOCKED = ''; private int $mermaidCounter = -1; private string $mermaidContent = ''; private bool $currentMermaidIsLocked = false; private string $mermaidContentIfLocked = ''; /** * Processes Gantt chart links in Mermaid diagrams * * Converts DokuWiki internal and external links into Mermaid-compatible * click events for Gantt chart tasks. * * @param array $instructions The parser instructions to process * @return array Modified instructions with properly formatted Gantt links */ private function processGanttLinks($instructions): array { $modified_instructions = $instructions; for ($i = 0; $i < count($modified_instructions); $i++) { if (!in_array($modified_instructions[$i][0], ['externallink', 'internallink'])) { continue; } // use the appropriate link $link = $modified_instructions[$i][0] === "externallink" ? $modified_instructions[$i][1][0] : wl($modified_instructions[$i][1][0], '', true); // change link here to just the name $modified_instructions[$i][0] = "cdata"; if($modified_instructions[$i][1][1] !== null) { unset($modified_instructions[$i][1][0]); } // insert the click event $click_reference = ''; if (preg_match('/(?<=:\s)\S+(?=,)/', $modified_instructions[$i+1][1][0], $output_array)) { $click_reference = $output_array[0]; } array_splice( $modified_instructions, $i + 2, 0, [["cdata", ["\nclick {$click_reference} href \"{$link}\"\n"]]] ); // encode colons if (isset($modified_instructions[$i][1][0]) && is_string($modified_instructions[$i][1][0])) { $modified_instructions[$i][1][0] = str_replace(":", "#colon;", $modified_instructions[$i][1][0]); } } return $modified_instructions; } /** * Protects DokuWiki link brackets from being processed * * @param string $text Text containing DokuWiki links * @return string Text with protected DokuWiki link brackets */ private function protectBracketsFromDokuWiki(string $text): string { $splitText = explode(self::DOKUWIKI_LINK_SPLITTER, $text); foreach ($splitText as $key => $line) { $splitText[$key] = preg_replace( '/(?Lexer->addEntryPattern( '(?=.*?)', $mode, 'plugin_mermaid'); } /** * Exit pattern for Mermaid * * @return void */ function postConnect(): void { $this->Lexer->addExitPattern( '', 'plugin_mermaid' ); } /** * Handles the matched text based on the lexer state * * @param string $match Matched text from the lexer * @param int $state Current lexer state (DOKU_LEXER_ENTER, DOKU_LEXER_UNMATCHED, DOKU_LEXER_EXIT) * @param Doku_Handler $handler DokuWiki handler instance * @return array Array with the state and processed match */ function handle($match, $state, $pos, Doku_Handler $handler): array { return [$state, $match]; } /** * Render xhtml output or metadata */ function render($mode, Doku_Renderer $renderer, $indata) { if($mode == 'xhtml'){ list($state, $match) = $indata; switch ($state) { case DOKU_LEXER_ENTER: $this->mermaidCounter++; $values = explode(" ", $match); $divwidth = count($values) < 2 ? 'auto' : $values[1]; $divheight = count($values) < 3 ? 'auto' : substr($values[2], 0, -1); $this->mermaidContent .= '
'; $this->mermaidContentIfLocked = $this->mermaidContent . 'mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">'; $this->mermaidContent .= 'mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">'; break; case DOKU_LEXER_UNMATCHED: $explodedMatch = explode("\n", $match); if(str_starts_with($explodedMatch[1], '%%currentMermaidIsLocked = true; $this->mermaidContent = $this->mermaidContentIfLocked . substr($explodedMatch[1], 2); break; } else { $this->currentMermaidIsLocked = false; } $israwmode = isset($explodedMatch[1]) && strpos($explodedMatch[1], 'raw') !== false; if($israwmode) { array_shift($explodedMatch); array_shift($explodedMatch); $actualContent = implode("\n", $explodedMatch); $this->mermaidContent .= $actualContent; } else { $instructions = $this->p_get_instructions($this->protectBracketsFromDokuWiki($match)); if (isset($instructions[2][1][0]) && is_string($instructions[2][1][0]) && strpos($instructions[2][1][0], "gantt")) { $instructions = $this->processGanttLinks($instructions); } $xhtml = $this->removeProtectionOfBracketsFromDokuWiki($this->p_render($instructions)); $this->mermaidContent .= preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $xhtml); } break; case DOKU_LEXER_EXIT: $this->mermaidContent .= "\r\n"; if($this->getConf('showSaveButton') or $this->getConf('showLockButton')) { $this->mermaidContent .= ''; } $this->mermaidContent .= '
'; $renderer->doc .= $this->mermaidContent; $this->mermaidContent = ''; break; } return true; } return false; } /* * Get the parser instructions suitable for the mermaid * */ function p_get_instructions($text) { //import parser classes and mode definitions require_once DOKU_INC . 'inc/parser/parser.php'; // https://www.dokuwiki.org/devel:parser // https://www.dokuwiki.org/devel:parser#basic_invocation // Create the parser and the handler $Parser = new Parser(new Doku_Handler()); $modes = array(); // add default modes $std_modes = array( 'internallink', 'media', 'externallink'); foreach($std_modes as $m) { $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m); $obj = new $class(); $modes[] = array( 'sort' => $obj->getSort(), 'mode' => $m, 'obj' => $obj ); } // add formatting modes $fmt_modes = array( 'strong', 'emphasis', 'underline', 'monospace', 'subscript', 'superscript', 'deleted'); foreach($fmt_modes as $m) { $obj = new \dokuwiki\Parsing\ParserMode\Formatting($m); $modes[] = array( 'sort' => $obj->getSort(), 'mode' => $m, 'obj' => $obj ); } //add modes to parser foreach($modes as $mode) { $Parser->addMode($mode['mode'],$mode['obj']); } // Do the parsing $p = $Parser->parse($text); return $p; } public function p_render($instructions) { $Renderer = p_get_renderer('mermaid'); // Loop through the instructions foreach ($instructions as $instruction) { if(method_exists($Renderer, $instruction[0])){ call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array()); } } return $Renderer->doc; } }