xref: /plugin/mermaid/syntax.php (revision 4b1a00230e86195217486377d94e7ab3993fdfd1)
1c6570b71SRobertWeinmeister<?php
2b566ae41SRobert Weinmeister
3c6570b71SRobertWeinmeister/**
4b566ae41SRobert 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
10b566ae41SRobert Weinmeisterdeclare(strict_types=1);
11b566ae41SRobert Weinmeister
12b566ae41SRobert Weinmeisterif (!defined('DOKU_INC'))
13b566ae41SRobert Weinmeister{
14b566ae41SRobert Weinmeister    die();
15b566ae41SRobert Weinmeister}
16ea08b541SRobert Weinmeister
17c6570b71SRobertWeinmeisteruse dokuwiki\Parsing\Parser;
18c6570b71SRobertWeinmeister
19c6570b71SRobertWeinmeisterclass syntax_plugin_mermaid extends \dokuwiki\Extension\SyntaxPlugin
20c6570b71SRobertWeinmeister{
21b566ae41SRobert Weinmeister    // Constants for DokuWiki link handling
22b566ae41SRobert Weinmeister    private const DOKUWIKI_LINK_START_MERMAID = '<code>DOKUWIKILINKSTARTMERMAID</code>';
23b566ae41SRobert Weinmeister    private const DOKUWIKI_LINK_END_MERMAID = '<code>DOKUWIKILINKENDMERMAID</code>';
24b566ae41SRobert Weinmeister    private const DOKUWIKI_LINK_SPLITTER ='--';
253543e422SRobert Weinmeister
26b566ae41SRobert Weinmeister    // SVG icons as constants
27b566ae41SRobert 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>';
28b566ae41SRobert 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>';
29b566ae41SRobert 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
31b566ae41SRobert Weinmeister    private int $mermaidCounter = -1;
32b566ae41SRobert Weinmeister    private string $mermaidContent = '';
33b566ae41SRobert Weinmeister    private bool $currentMermaidIsLocked = false;
34b566ae41SRobert Weinmeister    private string $mermaidContentIfLocked = '';
35b566ae41SRobert Weinmeister
36b566ae41SRobert Weinmeister    /**
37b566ae41SRobert Weinmeister     * Processes Gantt chart links in Mermaid diagrams
38b566ae41SRobert Weinmeister     *
39b566ae41SRobert Weinmeister     * Converts DokuWiki internal and external links into Mermaid-compatible
40b566ae41SRobert Weinmeister     * click events for Gantt chart tasks.
41b566ae41SRobert Weinmeister     *
42b566ae41SRobert Weinmeister     * @param array $instructions The parser instructions to process
43b566ae41SRobert Weinmeister     * @return array Modified instructions with properly formatted Gantt links
44b566ae41SRobert Weinmeister     */
45b566ae41SRobert 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        {
50b566ae41SRobert Weinmeister            if (!in_array($modified_instructions[$i][0], ['externallink', 'internallink'])) {
51b566ae41SRobert Weinmeister                continue;
52b566ae41SRobert Weinmeister            }
53da837cc9SRobert Weinmeister
54b566ae41SRobert Weinmeister            // use the appropriate link
55b566ae41SRobert Weinmeister            $link = $modified_instructions[$i][0] === "externallink"
56b566ae41SRobert Weinmeister                ? $modified_instructions[$i][1][0]
57b566ae41SRobert Weinmeister                : wl($modified_instructions[$i][1][0], '', true);
58b566ae41SRobert Weinmeister
59b566ae41SRobert Weinmeister            // change link here to just the name
60da837cc9SRobert Weinmeister            $modified_instructions[$i][0] = "cdata";
61b566ae41SRobert 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
66b566ae41SRobert Weinmeister            $click_reference = '';
67b566ae41SRobert Weinmeister            if (preg_match('/(?<=:\s)\S+(?=,)/', $modified_instructions[$i+1][1][0], $output_array)) {
68da837cc9SRobert Weinmeister                $click_reference = $output_array[0];
69da837cc9SRobert Weinmeister            }
70b566ae41SRobert Weinmeister
71b566ae41SRobert Weinmeister            array_splice(
72b566ae41SRobert Weinmeister                $modified_instructions,
73b566ae41SRobert Weinmeister                $i + 2,
74b566ae41SRobert Weinmeister                0,
75b566ae41SRobert Weinmeister                [["cdata", ["\nclick {$click_reference} href \"{$link}\"\n"]]]
76b566ae41SRobert Weinmeister            );
77da837cc9SRobert Weinmeister
78da837cc9SRobert Weinmeister            // encode colons
79b566ae41SRobert 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
87b566ae41SRobert Weinmeister    /**
88b566ae41SRobert Weinmeister     * Protects DokuWiki link brackets from being processed
89b566ae41SRobert Weinmeister     *
90b566ae41SRobert Weinmeister     * @param string $text Text containing DokuWiki links
91b566ae41SRobert Weinmeister     * @return string Text with protected DokuWiki link brackets
92b566ae41SRobert Weinmeister     */
93b566ae41SRobert Weinmeister    private function protectBracketsFromDokuWiki(string $text): string {
94f4ff867cSRobert Weinmeister        $splitText = explode(self::DOKUWIKI_LINK_SPLITTER, $text);
95b566ae41SRobert Weinmeister        foreach ($splitText as $key => $line) {
96b566ae41SRobert Weinmeister            $splitText[$key] = preg_replace(
97b566ae41SRobert Weinmeister                '/(?<!["\[(\s])(\[\[)(.*)(\]\])/',
98b566ae41SRobert Weinmeister                self::DOKUWIKI_LINK_START_MERMAID . '$2' . self::DOKUWIKI_LINK_END_MERMAID,
99b566ae41SRobert Weinmeister                $line
100b566ae41SRobert Weinmeister            ) ?? $line;
101f4ff867cSRobert Weinmeister        }
102b566ae41SRobert Weinmeister        return implode(self::DOKUWIKI_LINK_SPLITTER, $splitText);
1033543e422SRobert Weinmeister    }
1043543e422SRobert Weinmeister
105b566ae41SRobert Weinmeister    /**
106b566ae41SRobert Weinmeister    * Reverts the protection of DokuWiki link brackets
107b566ae41SRobert Weinmeister    *
108b566ae41SRobert Weinmeister    * @param string $text Text containing protected DokuWiki links
109b566ae41SRobert Weinmeister    * @return string Text with restored DokuWiki link brackets
110b566ae41SRobert Weinmeister    */
111b566ae41SRobert Weinmeister    private function removeProtectionOfBracketsFromDokuWiki(string $text): string {
112b566ae41SRobert Weinmeister        return str_replace(
113b566ae41SRobert Weinmeister            [self::DOKUWIKI_LINK_START_MERMAID, self::DOKUWIKI_LINK_END_MERMAID],
114b566ae41SRobert Weinmeister            ['[[',  ']]'],
115b566ae41SRobert Weinmeister            $text
116b566ae41SRobert Weinmeister        );
1173543e422SRobert Weinmeister    }
1183543e422SRobert Weinmeister
119c6570b71SRobertWeinmeister    /** @inheritDoc */
120b566ae41SRobert Weinmeister    function getType(): string {
121c6570b71SRobertWeinmeister        return 'container';
122c6570b71SRobertWeinmeister    }
123c6570b71SRobertWeinmeister
124c6570b71SRobertWeinmeister    /** @inheritDoc */
125b566ae41SRobert Weinmeister    function getSort(): int {
126c6570b71SRobertWeinmeister        return 150;
127c6570b71SRobertWeinmeister    }
128c6570b71SRobertWeinmeister
129c6570b71SRobertWeinmeister    /**
130b566ae41SRobert Weinmeister    * Entry pattern for Mermaid
131c6570b71SRobertWeinmeister    *
132c6570b71SRobertWeinmeister    * @param string $mode Parser mode
133b566ae41SRobert Weinmeister    * @return void
134c6570b71SRobertWeinmeister    */
135b566ae41SRobert Weinmeister    public function connectTo($mode): void {
136b566ae41SRobert Weinmeister        $this->Lexer->addEntryPattern(
137b566ae41SRobert Weinmeister            '<mermaid.*?>(?=.*?</mermaid>)',
138b566ae41SRobert Weinmeister            $mode,
139b566ae41SRobert Weinmeister            'plugin_mermaid');
140c6570b71SRobertWeinmeister    }
141c6570b71SRobertWeinmeister
142c6570b71SRobertWeinmeister    /**
143b566ae41SRobert Weinmeister     * Exit pattern for Mermaid
144b566ae41SRobert Weinmeister     *
145b566ae41SRobert Weinmeister     * @return void
146c6570b71SRobertWeinmeister     */
147b566ae41SRobert Weinmeister    function postConnect(): void {
148b566ae41SRobert Weinmeister        $this->Lexer->addExitPattern(
149b566ae41SRobert Weinmeister            '</mermaid>',
150b566ae41SRobert Weinmeister            'plugin_mermaid'
151b566ae41SRobert Weinmeister        );
152c6570b71SRobertWeinmeister    }
153b566ae41SRobert Weinmeister
154b566ae41SRobert Weinmeister    /**
155b566ae41SRobert Weinmeister     * Handles the matched text based on the lexer state
156b566ae41SRobert Weinmeister     *
157b566ae41SRobert Weinmeister     * @param string $match Matched text from the lexer
158b566ae41SRobert Weinmeister     * @param int $state Current lexer state (DOKU_LEXER_ENTER, DOKU_LEXER_UNMATCHED, DOKU_LEXER_EXIT)
159b566ae41SRobert Weinmeister     * @param Doku_Handler $handler DokuWiki handler instance
160b566ae41SRobert Weinmeister     * @return array Array with the state and processed match
161b566ae41SRobert Weinmeister     */
162b566ae41SRobert Weinmeister    function handle($match, $state, $pos, Doku_Handler $handler): array {
163b566ae41SRobert 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                    {
207b566ae41SRobert Weinmeister                        $instructions = $this->p_get_instructions($this->protectBracketsFromDokuWiki($match));
208da837cc9SRobert Weinmeister                        if (strpos($instructions[2][1][0], "gantt"))
209da837cc9SRobert Weinmeister                        {
210b566ae41SRobert Weinmeister                            $instructions = $this->processGanttLinks($instructions);
211da837cc9SRobert Weinmeister                        }
212b566ae41SRobert 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>";
218*4b1a0023Sbrzsmg                    if($this->getConf('showSaveButton') or $this->getConf('showLockButton'))
219*4b1a0023Sbrzsmg                    {
220172fa282SRobert Weinmeister                        $this->mermaidContent .= '<fieldset id="mermaidFieldset'.$this->mermaidCounter.'" style="position: absolute; top: 0; left: 0; display: none; width:auto; border: none">';
221*4b1a0023Sbrzsmg                        if($this->getConf('showSaveButton'))
222*4b1a0023Sbrzsmg                        {
223172fa282SRobert 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>';
224*4b1a0023Sbrzsmg                        }
225*4b1a0023Sbrzsmg                        if($this->getConf('showLockButton'))
226*4b1a0023Sbrzsmg                        {
227172fa282SRobert 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>';
228*4b1a0023Sbrzsmg                        }
229*4b1a0023Sbrzsmg                        $this->mermaidContent .= '</fieldset>';
230*4b1a0023Sbrzsmg                    }
231*4b1a0023Sbrzsmg                    $this->mermaidContent .= '</div>';
232172fa282SRobert Weinmeister
233172fa282SRobert Weinmeister                    $renderer->doc .= $this->mermaidContent;
234172fa282SRobert Weinmeister                    $this->mermaidContent = '';
235c6570b71SRobertWeinmeister                break;
236c6570b71SRobertWeinmeister            }
237c6570b71SRobertWeinmeister            return true;
238c6570b71SRobertWeinmeister        }
239c6570b71SRobertWeinmeister        return false;
240c6570b71SRobertWeinmeister    }
241c6570b71SRobertWeinmeister
242c6570b71SRobertWeinmeister    /*
2437d8a2661SRobert Weinmeister    * Get the parser instructions suitable for the mermaid
244c6570b71SRobertWeinmeister    *
245c6570b71SRobertWeinmeister    */
246c6570b71SRobertWeinmeister    function p_get_instructions($text)
247c6570b71SRobertWeinmeister    {
248c6570b71SRobertWeinmeister        //import parser classes and mode definitions
249c6570b71SRobertWeinmeister        require_once DOKU_INC . 'inc/parser/parser.php';
250c6570b71SRobertWeinmeister
251c6570b71SRobertWeinmeister        // https://www.dokuwiki.org/devel:parser
252c6570b71SRobertWeinmeister        // https://www.dokuwiki.org/devel:parser#basic_invocation
253c6570b71SRobertWeinmeister        // Create the parser and the handler
254c6570b71SRobertWeinmeister        $Parser = new Parser(new Doku_Handler());
255c6570b71SRobertWeinmeister
256c6570b71SRobertWeinmeister        $modes = array();
257c6570b71SRobertWeinmeister
258c6570b71SRobertWeinmeister        // add default modes
259c6570b71SRobertWeinmeister        $std_modes = array( 'internallink', 'media', 'externallink');
260c6570b71SRobertWeinmeister
2613543e422SRobert Weinmeister        foreach($std_modes as $m)
2623543e422SRobert Weinmeister        {
263c6570b71SRobertWeinmeister            $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m);
264c6570b71SRobertWeinmeister            $obj   = new $class();
265c6570b71SRobertWeinmeister            $modes[] = array(
266c6570b71SRobertWeinmeister            'sort' => $obj->getSort(),
267c6570b71SRobertWeinmeister            'mode' => $m,
268c6570b71SRobertWeinmeister            'obj'  => $obj
269c6570b71SRobertWeinmeister            );
270c6570b71SRobertWeinmeister        }
271c6570b71SRobertWeinmeister
272c6570b71SRobertWeinmeister        // add formatting modes
273c6570b71SRobertWeinmeister        $fmt_modes = array( 'strong', 'emphasis', 'underline', 'monospace', 'subscript', 'superscript', 'deleted');
274c6570b71SRobertWeinmeister        foreach($fmt_modes as $m)
275c6570b71SRobertWeinmeister        {
276c6570b71SRobertWeinmeister          $obj   = new \dokuwiki\Parsing\ParserMode\Formatting($m);
277c6570b71SRobertWeinmeister          $modes[] = array(
278c6570b71SRobertWeinmeister            'sort' => $obj->getSort(),
279c6570b71SRobertWeinmeister            'mode' => $m,
280c6570b71SRobertWeinmeister            'obj'  => $obj
281c6570b71SRobertWeinmeister          );
282c6570b71SRobertWeinmeister        }
283c6570b71SRobertWeinmeister
284c6570b71SRobertWeinmeister        //add modes to parser
285c6570b71SRobertWeinmeister        foreach($modes as $mode)
286c6570b71SRobertWeinmeister        {
287c6570b71SRobertWeinmeister            $Parser->addMode($mode['mode'],$mode['obj']);
288c6570b71SRobertWeinmeister        }
289c6570b71SRobertWeinmeister
290c6570b71SRobertWeinmeister        // Do the parsing
291c6570b71SRobertWeinmeister        $p = $Parser->parse($text);
292c6570b71SRobertWeinmeister
293c6570b71SRobertWeinmeister        return $p;
294c6570b71SRobertWeinmeister    }
295c6570b71SRobertWeinmeister
296c6570b71SRobertWeinmeister    public function p_render($instructions)
297c6570b71SRobertWeinmeister    {
298b566ae41SRobert Weinmeister
299c6570b71SRobertWeinmeister        $Renderer = p_get_renderer('mermaid');
300c6570b71SRobertWeinmeister
301c6570b71SRobertWeinmeister        // Loop through the instructions
302c6570b71SRobertWeinmeister        foreach ($instructions as $instruction) {
303c6570b71SRobertWeinmeister            if(method_exists($Renderer, $instruction[0])){
304c6570b71SRobertWeinmeister            call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array());
305c6570b71SRobertWeinmeister            }
306c6570b71SRobertWeinmeister        }
307c6570b71SRobertWeinmeister        return $Renderer->doc;
308c6570b71SRobertWeinmeister    }
309c6570b71SRobertWeinmeister}