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