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