xref: /plugin/mermaid/syntax.php (revision 172fa2826b77d3e839826cc5bf24ae2f2508ea13)
1<?php
2/**
3 * DokuWiki Plugin mermaid (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Robert Weinmeister <develop@weinmeister.org>
7 */
8
9use dokuwiki\Parsing\Parser;
10
11class syntax_plugin_mermaid extends \dokuwiki\Extension\SyntaxPlugin
12{
13    const DOKUWIKI_LINK_START_MERMAID = '<code>DOKUWIKILINKSTARTMERMAID</code>';
14    const DOKUWIKI_LINK_END_MERMAID = '<code>DOKUWIKILINKENDMERMAID</code>';
15    const DOKUWIKI_LINK_SPLITTER ='--';
16    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>';
17    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>';
18    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>';
19
20    private $mermaidCounter = -1;
21    private $mermaidContent;
22    private $currentMermaidIsLocked = false;
23    private $mermaidContentIfLocked;
24
25    function enable_gantt_links($instructions)
26    {
27        $modified_instructions = $instructions;
28
29        for ($i = 0; $i < count($modified_instructions); $i++)
30        {
31            if (in_array($modified_instructions[$i][0], ["externallink", "internallink"]))
32            {
33                // use the appropriate link
34                $link = $modified_instructions[$i][0] == "externallink" ? $modified_instructions[$i][1][0] : wl($modified_instructions[$i][1][0], '', true);
35
36                // change link here to just the name of the link
37                $modified_instructions[$i][0]= "cdata";
38                if(!is_null($modified_instructions[$i][1][1]))
39                {
40                    unset($modified_instructions[$i][1][0]);
41                }
42
43                // insert the click event
44                if (preg_match('/(?<=:\s)\S+(?=,)/', $modified_instructions[$i+1][1][0], $output_array))
45                {
46                    $click_reference = $output_array[0];
47                }
48                array_splice($modified_instructions, $i + 2, 0, [["cdata", ["\nclick ".$click_reference." href \"".$link."\"\n"]]]);
49
50                // encode colons
51                $modified_instructions[$i][1][0] = str_replace(":", "#colon;", $modified_instructions[$i][1][0]);
52            }
53        }
54
55        return $modified_instructions;
56    }
57
58    function protect_brackets_from_dokuwiki($text)
59    {
60        $splitText = explode(self::DOKUWIKI_LINK_SPLITTER, $text);
61        foreach ($splitText as $key => $line)
62        {
63            $splitText[$key] = preg_replace('/(?<!["\[(\s])(\[\[)(.*)(\]\])/', self::DOKUWIKI_LINK_START_MERMAID . '$2' . self::DOKUWIKI_LINK_END_MERMAID, $line);
64        }
65        $text = implode(self::DOKUWIKI_LINK_SPLITTER, $splitText);
66        return $text;
67    }
68
69    function remove_protection_of_brackets_from_dokuwiki($text)
70    {
71        return str_replace(self::DOKUWIKI_LINK_START_MERMAID, '[[', str_replace(self::DOKUWIKI_LINK_END_MERMAID, ']]', $text));
72    }
73
74    /** @inheritDoc */
75    function getType()
76    {
77        return 'container';
78    }
79
80    /** @inheritDoc */
81    function getSort()
82    {
83        return 150;
84    }
85
86    /**
87    * Connect lookup pattern to lexer.
88    *
89    * @param string $mode Parser mode
90    */
91    function connectTo($mode)
92    {
93        $this->Lexer->addEntryPattern('<mermaid.*?>(?=.*?</mermaid>)',$mode,'plugin_mermaid');
94    }
95
96    function postConnect()
97    {
98        $this->Lexer->addExitPattern('</mermaid>','plugin_mermaid');
99    }
100
101    /**
102    * Handle matches of the Mermaid syntax
103    */
104    function handle($match, $state, $pos, Doku_Handler $handler)
105    {
106        switch ($state)
107        {
108            case DOKU_LEXER_ENTER:
109            return array($state, $match);
110            case DOKU_LEXER_UNMATCHED:
111            return array($state, $match);
112            case DOKU_LEXER_EXIT:
113            return array($state, '');
114        }
115        return false;
116    }
117
118    /**
119    * Render xhtml output or metadata
120    */
121    function render($mode, Doku_Renderer $renderer, $indata)
122    {
123        if($mode == 'xhtml'){
124            list($state, $match) = $indata;
125            switch ($state) {
126                case DOKU_LEXER_ENTER:
127                    $this->mermaidCounter++;
128                    $values = explode(" ", $match);
129                    $divwidth = count($values) < 2 ? 'auto' : $values[1];
130                    $divheight = count($values) < 3 ? 'auto' : substr($values[2], 0, -1);
131                    $this->mermaidContent .= '<div id="mermaidContainer'.$this->mermaidCounter.'" style="position: relative; width:'.$divwidth.'; height:'.$divheight.'">';
132                    $this->mermaidContentIfLocked = $this->mermaidContent . '<span class="mermaidlocked" id=mermaidContent'.$this->mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">';
133                    $this->mermaidContent .= '<span class="mermaid" id=mermaidContent'.$this->mermaidCounter.' style="width:'.$divwidth.'; height:'.$divheight.'">';
134                break;
135                case DOKU_LEXER_UNMATCHED:
136                    $explodedMatch = explode("\n", $match);
137
138                    if(str_starts_with($explodedMatch[1], '%%<svg'))
139                    {
140                        $this->currentMermaidIsLocked = true;
141                        $this->mermaidContent = $this->mermaidContentIfLocked . substr($explodedMatch[1], 2);
142                        break;
143                    }
144                    else
145                    {
146                        $this->currentMermaidIsLocked = false;
147                    }
148
149                    $israwmode = isset($explodedMatch[1]) && strpos($explodedMatch[1], 'raw') !== false;
150                    if($israwmode)
151                    {
152                        array_shift($explodedMatch);
153                        array_shift($explodedMatch);
154                        $actualContent = implode("\n", $explodedMatch);
155                        $this->mermaidContent .= $actualContent;
156                    }
157                    else
158                    {
159                        $instructions = $this->p_get_instructions($this->protect_brackets_from_dokuwiki($match));
160                        if (strpos($instructions[2][1][0], "gantt"))
161                        {
162                            $instructions = $this->enable_gantt_links($instructions);
163                        }
164                        $xhtml = $this->remove_protection_of_brackets_from_dokuwiki($this->p_render($instructions));
165                        $this->mermaidContent .= preg_replace("/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/", "\n", $xhtml);
166                    }
167                break;
168                case DOKU_LEXER_EXIT:
169                    $this->mermaidContent .= "\r\n</span>";
170                    $this->mermaidContent .= '<fieldset id="mermaidFieldset'.$this->mermaidCounter.'" style="position: absolute; top: 0; left: 0; display: none; width:auto; border: none">';
171                    $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>';
172                    $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>';
173                    $this->mermaidContent .= '</fieldset></div>';
174
175                    $renderer->doc .= $this->mermaidContent;
176                    $this->mermaidContent = '';
177                break;
178            }
179            return true;
180        }
181        return false;
182    }
183
184    /*
185    * Get the parser instructions suitable for the mermaid
186    *
187    */
188    function p_get_instructions($text)
189    {
190        //import parser classes and mode definitions
191        require_once DOKU_INC . 'inc/parser/parser.php';
192
193        // https://www.dokuwiki.org/devel:parser
194        // https://www.dokuwiki.org/devel:parser#basic_invocation
195        // Create the parser and the handler
196        $Parser = new Parser(new Doku_Handler());
197
198        $modes = array();
199
200        // add default modes
201        $std_modes = array( 'internallink', 'media', 'externallink');
202
203        foreach($std_modes as $m)
204        {
205            $class = 'dokuwiki\\Parsing\\ParserMode\\'.ucfirst($m);
206            $obj   = new $class();
207            $modes[] = array(
208            'sort' => $obj->getSort(),
209            'mode' => $m,
210            'obj'  => $obj
211            );
212        }
213
214        // add formatting modes
215        $fmt_modes = array( 'strong', 'emphasis', 'underline', 'monospace', 'subscript', 'superscript', 'deleted');
216        foreach($fmt_modes as $m)
217        {
218          $obj   = new \dokuwiki\Parsing\ParserMode\Formatting($m);
219          $modes[] = array(
220            'sort' => $obj->getSort(),
221            'mode' => $m,
222            'obj'  => $obj
223          );
224        }
225
226        //add modes to parser
227        foreach($modes as $mode)
228        {
229            $Parser->addMode($mode['mode'],$mode['obj']);
230        }
231
232        // Do the parsing
233        $p = $Parser->parse($text);
234
235        return $p;
236    }
237
238    public function p_render($instructions)
239    {
240        $Renderer = p_get_renderer('mermaid');
241
242        // Loop through the instructions
243        foreach ($instructions as $instruction) {
244            if(method_exists($Renderer, $instruction[0])){
245            call_user_func_array(array(&$Renderer, $instruction[0]), $instruction[1] ? $instruction[1] : array());
246            }
247        }
248
249        return $Renderer->doc;
250    }
251}