xref: /plugin/mermaid/action.php (revision 172fa2826b77d3e839826cc5bf24ae2f2508ea13)
1<?php
2/**
3 * DokuWiki Plugin mermaid (Action Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Robert Weinmeister <develop@weinmeister.org>
7 */
8
9class action_plugin_mermaid extends \dokuwiki\Extension\ActionPlugin
10{
11    /** @inheritDoc */
12    public function register(Doku_Event_Handler $controller)
13    {
14        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load');
15        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxRequest');
16    }
17
18    public function handleAjaxRequest(Doku_Event $event, $param) {
19        if ($event->data !== 'plugin_mermaid')
20        {
21            return;
22        }
23
24        $event->stopPropagation();
25        $event->preventDefault();
26
27        $ID = cleanID(urldecode($_REQUEST['pageid']));
28
29        if(auth_quickaclcheck($ID) < AUTH_EDIT)
30        {
31            echo json_encode(['status' => 'failure', 'data' => ['You do not have permission to edit this file.\nAccess was denied.']]);
32            exit();
33        }
34
35        if(checklock($ID))
36        {
37            echo json_encode(['status' => 'failure', 'data' => ['The page is currently locked.\nTry again later.']]);
38            exit();
39        }
40
41        $wikitext = rawWiki($ID);
42        $newWikitext = $wikitext;
43
44        if($_REQUEST['mode'] == 'lock')
45        {
46            preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE);
47
48            if(is_array($matches) && count($matches[0]) > $_REQUEST['mermaidindex'])
49            {
50                $whereToInsert = $matches[1][$_REQUEST['mermaidindex']][1];
51                $newWikitext = substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert);
52            }
53            else
54            {
55                echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]);
56                exit();
57            }
58        }
59
60        if($_REQUEST['mode'] == 'unlock')
61        {
62            $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count);
63            if($count != 1)
64            {
65                echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]);
66                exit();
67            }
68        }
69
70        if(strlen($newWikitext) > 0 && $newWikitext != $wikitext)
71        {
72            lock($ID);
73            saveWikiText($ID, $newWikitext, $_REQUEST['mode'] . ' Mermaid diagram', $minoredit = true);
74            unlock($ID);
75
76            echo json_encode(['status' => 'success', 'data' => []]);
77            exit();
78        }
79
80        echo json_encode(['status' => 'failure', 'data' => ['Could not '.$_REQUEST['mode'].' the Mermaid diagram.']]);
81        exit();
82    }
83
84    public function load(Doku_Event $event, $param)
85    {
86        // Can be changed for debugging Mermaid
87        // https://mermaid.js.org/config/directives.html#changing-loglevel-via-directive
88        define("MERMAIDLOGLEVEL", "error");
89
90        $theme = $this->getConf('theme');
91        $init = "mermaid.initialize({startOnLoad: true, logLevel: '".MERMAIDLOGLEVEL."', theme: '".$theme."'});";
92        $location = $this->getConf('location');
93
94        switch ($location) {
95            case 'local':
96                $event->data['script'][] = array
97                (
98                    'type'    => 'text/javascript',
99                    'charset' => 'utf-8',
100                    'src' => DOKU_BASE.'lib/plugins/mermaid/mermaid.min.js'
101                );
102                break;
103            case 'latest':
104            case 'remote1091':
105            // options remote108, remote106, remote104, remote103, remote102, remote101, remote100 are depreciated and only included for backward compatibility
106            case 'remote108':
107            case 'remote106':
108            case 'remote104':
109            case 'remote103':
110            case 'remote102':
111            case 'remote101':
112            case 'remote100':
113                $versions = array(
114                    'latest' => '',
115                    'remote1091' => '@10.9.1',
116                    'remote108' => '@10.8.0',
117                    'remote106' => '@10.6.1',
118                    'remote104' => '@10.4.0',
119                    'remote103' => '@10.3.1',
120                    'remote102' => '@10.2.4',
121                    'remote101' => '@10.1.0',
122                    'remote100' => '@10.0.2'
123                );
124                $data = "import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid".$versions[$location]."/dist/mermaid.esm.min.mjs';".$init;
125                $event->data['script'][] = array
126                (
127                    'type'    => 'module',
128                    'charset' => 'utf-8',
129                    '_data' => $data
130                );
131                break;
132            // option remote94 is depreciated and only included for backward compatibility
133            case 'remote94':
134            case 'remote943':
135                $event->data['script'][] = array
136                (
137                    'type'    => 'text/javascript',
138                    'charset' => 'utf-8',
139                    'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.4.3/dist/mermaid.min.js'
140                );
141                break;
142            // option remote93 is depreciated and only included for backward compatibility
143            case 'remote93':
144                $event->data['script'][] = array
145                (
146                    'type'    => 'text/javascript',
147                    'charset' => 'utf-8',
148                    'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.3.0/dist/mermaid.min.js'
149                );
150                break;
151            default:
152        }
153
154        $event->data['link'][] = array
155        (
156            'rel'     => 'stylesheet',
157            'type'    => 'text/css',
158            'href'    => DOKU_BASE."lib/plugins/mermaid/mermaid.css",
159        );
160
161        switch ($location) {
162            case 'local':
163            case 'remote943':
164            // options remote94 and remote93 are depreciated and only included for backward compatibility
165            case 'remote94':
166            case 'remote93':
167                $event->data['script'][] = array
168                (
169                    'type'    => 'text/javascript',
170                    'charset' => 'utf-8',
171                    '_data'   => $init
172                );
173                break;
174            default:
175        }
176
177        // remove the search highlight from DokuWiki as it interferes with the Mermaid parsing/rendering
178        $event->data['script'][] = array
179        (
180            'type'    => 'text/javascript',
181            'charset' => 'utf-8',
182            '_data' => "document.addEventListener('DOMContentLoaded', function() {
183                            jQuery('.mermaid').each(function() {
184                                var modifiedContent = jQuery(this).html().replace(/<span class=\"search_hit\">(.+?)<\/span>/g, '$1');
185                                jQuery(this).html(modifiedContent);
186                             })
187                        });"
188        );
189
190        // adds image-save capability
191        // First: Wait until the DOM content is fully loaded
192        // Second: Wait until Mermaid has changed the dokuwiki content to an svg
193        $event->data['script'][] = array
194        (
195            'type'    => 'text/javascript',
196            'charset' => 'utf-8',
197            '_data' => "
198document.addEventListener('DOMContentLoaded', function() {
199     var config = {
200        childList: true,
201        subtree: true,
202        characterData: true
203    };
204
205    function callDokuWikiPHP(mode, index, mermaidRaw, mermaidSvg) {
206    jQuery.post(
207        DOKU_BASE + 'lib/exe/ajax.php',
208        {
209            call: 'plugin_mermaid',
210            mode: mode,
211            mermaidindex: index,
212            pageid: '".getID()."',
213            svg: encodeURIComponent(mermaidSvg)
214        },
215        function(response) {
216            if(response.status == 'success') {
217                location.reload(true);
218            }
219            else {
220                alert(response.data[0]);
221            }
222        },
223        'json'
224    )};
225
226    jQuery('.mermaidlocked, .mermaid').each(function(index, element) {
227        document.getElementById('mermaidContainer' + index).addEventListener('mouseenter', function() {
228             document.getElementById('mermaidFieldset' + index).style.display = 'flex';
229        });
230        document.getElementById('mermaidContainer' + index).addEventListener('mouseleave', function() {
231            document.getElementById('mermaidFieldset' + index).style.display = 'none';
232        });
233
234        if(jQuery(element).hasClass('mermaidlocked')) {
235            document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => {
236                var svgContent = element.innerHTML.trim();
237                var blob = new Blob([svgContent], { type: 'image/svg+xml' });
238                var link = document.createElement('a');
239                link.href = URL.createObjectURL(blob);
240                link.download = 'mermaid' + index + '.svg';
241                link.click();
242                URL.revokeObjectURL(link.href);
243            });
244
245            document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => {
246                if(confirm('Unlock Mermaid diagram?')) {
247                    callDokuWikiPHP('unlock', index, originalMermaidContent, element.innerHTML.trim());
248                }
249            });
250        }
251
252        if(jQuery(element).hasClass('mermaid')) {
253            var originalMermaidContent = element.innerHTML;
254            var observer = new MutationObserver(function(mutations) {
255                mutations.forEach(function(mutation) {
256                    if (mutation.type === 'childList' && element.innerHTML.startsWith('<svg')) {
257                        document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => {
258                            var svgContent = element.innerHTML.trim();
259                            var blob = new Blob([svgContent], { type: 'image/svg+xml' });
260                            var link = document.createElement('a');
261                            link.href = URL.createObjectURL(blob);
262                            link.download = 'mermaid' + index + '.svg';
263                            link.click();
264                            URL.revokeObjectURL(link.href);
265                        });
266
267                       document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => {
268                            if(confirm('Lock Mermaid diagram? [experimental]')) {
269                                callDokuWikiPHP('lock', index, originalMermaidContent, element.innerHTML.trim());
270                            }
271                       });
272                    }
273                });
274            });
275            observer.observe(element, config);
276        }
277    });
278});"
279        );
280    }
281}
282
283