xref: /plugin/mermaid/syntax.php (revision b566ae41d43f34982a49c783a4196e63d98ad341)
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}