xref: /plugin/mermaid/action.php (revision ea08b541b91ff4c3ad221bf3b0e2cb9c1cb7688d)
146a60b4fSRobertWeinmeister<?php
246a60b4fSRobertWeinmeister/**
346a60b4fSRobertWeinmeister * DokuWiki Plugin mermaid (Action Component)
446a60b4fSRobertWeinmeister *
546a60b4fSRobertWeinmeister * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
646a60b4fSRobertWeinmeister * @author  Robert Weinmeister <develop@weinmeister.org>
746a60b4fSRobertWeinmeister */
846a60b4fSRobertWeinmeister
9*ea08b541SRobert Weinmeisterif (!defined('DOKU_INC')) die();
10*ea08b541SRobert Weinmeister
1146a60b4fSRobertWeinmeisterclass action_plugin_mermaid extends \dokuwiki\Extension\ActionPlugin
1246a60b4fSRobertWeinmeister{
1346a60b4fSRobertWeinmeister    /** @inheritDoc */
1446a60b4fSRobertWeinmeister    public function register(Doku_Event_Handler $controller)
1546a60b4fSRobertWeinmeister    {
1646a60b4fSRobertWeinmeister        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load');
17172fa282SRobert Weinmeister        $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxRequest');
18172fa282SRobert Weinmeister    }
19172fa282SRobert Weinmeister
20172fa282SRobert Weinmeister    public function handleAjaxRequest(Doku_Event $event, $param) {
21172fa282SRobert Weinmeister        if ($event->data !== 'plugin_mermaid')
22172fa282SRobert Weinmeister        {
23172fa282SRobert Weinmeister            return;
24172fa282SRobert Weinmeister        }
25172fa282SRobert Weinmeister
26172fa282SRobert Weinmeister        $event->stopPropagation();
27172fa282SRobert Weinmeister        $event->preventDefault();
28172fa282SRobert Weinmeister
29172fa282SRobert Weinmeister        $ID = cleanID(urldecode($_REQUEST['pageid']));
30172fa282SRobert Weinmeister
31172fa282SRobert Weinmeister        if(auth_quickaclcheck($ID) < AUTH_EDIT)
32172fa282SRobert Weinmeister        {
33172fa282SRobert Weinmeister            echo json_encode(['status' => 'failure', 'data' => ['You do not have permission to edit this file.\nAccess was denied.']]);
34172fa282SRobert Weinmeister            exit();
35172fa282SRobert Weinmeister        }
36172fa282SRobert Weinmeister
37172fa282SRobert Weinmeister        if(checklock($ID))
38172fa282SRobert Weinmeister        {
39172fa282SRobert Weinmeister            echo json_encode(['status' => 'failure', 'data' => ['The page is currently locked.\nTry again later.']]);
40172fa282SRobert Weinmeister            exit();
41172fa282SRobert Weinmeister        }
42172fa282SRobert Weinmeister
43172fa282SRobert Weinmeister        $wikitext = rawWiki($ID);
44172fa282SRobert Weinmeister        $newWikitext = $wikitext;
45172fa282SRobert Weinmeister
46172fa282SRobert Weinmeister        if($_REQUEST['mode'] == 'lock')
47172fa282SRobert Weinmeister        {
48172fa282SRobert Weinmeister            preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE);
49172fa282SRobert Weinmeister
50172fa282SRobert Weinmeister            if(is_array($matches) && count($matches[0]) > $_REQUEST['mermaidindex'])
51172fa282SRobert Weinmeister            {
52172fa282SRobert Weinmeister                $whereToInsert = $matches[1][$_REQUEST['mermaidindex']][1];
53172fa282SRobert Weinmeister                $newWikitext = substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert);
54172fa282SRobert Weinmeister            }
55172fa282SRobert Weinmeister            else
56172fa282SRobert Weinmeister            {
57172fa282SRobert Weinmeister                echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]);
58172fa282SRobert Weinmeister                exit();
59172fa282SRobert Weinmeister            }
60172fa282SRobert Weinmeister        }
61172fa282SRobert Weinmeister
62172fa282SRobert Weinmeister        if($_REQUEST['mode'] == 'unlock')
63172fa282SRobert Weinmeister        {
64172fa282SRobert Weinmeister            $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count);
65172fa282SRobert Weinmeister            if($count != 1)
66172fa282SRobert Weinmeister            {
67172fa282SRobert Weinmeister                echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]);
68172fa282SRobert Weinmeister                exit();
69172fa282SRobert Weinmeister            }
70172fa282SRobert Weinmeister        }
71172fa282SRobert Weinmeister
72172fa282SRobert Weinmeister        if(strlen($newWikitext) > 0 && $newWikitext != $wikitext)
73172fa282SRobert Weinmeister        {
74172fa282SRobert Weinmeister            lock($ID);
75172fa282SRobert Weinmeister            saveWikiText($ID, $newWikitext, $_REQUEST['mode'] . ' Mermaid diagram', $minoredit = true);
76172fa282SRobert Weinmeister            unlock($ID);
77172fa282SRobert Weinmeister
78172fa282SRobert Weinmeister            echo json_encode(['status' => 'success', 'data' => []]);
79172fa282SRobert Weinmeister            exit();
80172fa282SRobert Weinmeister        }
81172fa282SRobert Weinmeister
82172fa282SRobert Weinmeister        echo json_encode(['status' => 'failure', 'data' => ['Could not '.$_REQUEST['mode'].' the Mermaid diagram.']]);
83172fa282SRobert Weinmeister        exit();
8446a60b4fSRobertWeinmeister    }
8546a60b4fSRobertWeinmeister
8646a60b4fSRobertWeinmeister    public function load(Doku_Event $event, $param)
8746a60b4fSRobertWeinmeister    {
88*ea08b541SRobert Weinmeister        // only load mermaid if it is needed
89*ea08b541SRobert Weinmeister        if(strpos(rawWiki(getID()), '<mermaid') === false)
90*ea08b541SRobert Weinmeister        {
91*ea08b541SRobert Weinmeister            return;
92*ea08b541SRobert Weinmeister        }
93*ea08b541SRobert Weinmeister
946e5341c6SRobert Weinmeister        // Can be changed for debugging Mermaid
956e5341c6SRobert Weinmeister        // https://mermaid.js.org/config/directives.html#changing-loglevel-via-directive
966e5341c6SRobert Weinmeister        define("MERMAIDLOGLEVEL", "error");
976e5341c6SRobert Weinmeister
984c8bd9ffSRobert Weinmeister        $theme = $this->getConf('theme');
994c8bd9ffSRobert Weinmeister        $init = "mermaid.initialize({startOnLoad: true, logLevel: '".MERMAIDLOGLEVEL."', theme: '".$theme."'});";
1006fcac025SRobert Weinmeister        $location = $this->getConf('location');
1014c8bd9ffSRobert Weinmeister
1026fcac025SRobert Weinmeister        switch ($location) {
1032d4b7fc2SRobert Weinmeister            case 'local':
10446a60b4fSRobertWeinmeister                $event->data['script'][] = array
10546a60b4fSRobertWeinmeister                (
10646a60b4fSRobertWeinmeister                    'type'    => 'text/javascript',
10746a60b4fSRobertWeinmeister                    'charset' => 'utf-8',
1082d4b7fc2SRobert Weinmeister                    'src' => DOKU_BASE.'lib/plugins/mermaid/mermaid.min.js'
10946a60b4fSRobertWeinmeister                );
1102d4b7fc2SRobert Weinmeister                break;
1112d4b7fc2SRobert Weinmeister            case 'latest':
112a788b843SRobert Weinmeister            case 'remote1091':
113a788b843SRobert Weinmeister            // options remote108, remote106, remote104, remote103, remote102, remote101, remote100 are depreciated and only included for backward compatibility
1146fcac025SRobert Weinmeister            case 'remote108':
115a612c7d6SRobert Weinmeister            case 'remote106':
1168eaa3f3bSRobert Weinmeister            case 'remote104':
1177d8a2661SRobert Weinmeister            case 'remote103':
1184df3d176SRobert Weinmeister            case 'remote102':
1195f50b169SRobert Weinmeister            case 'remote101':
1206e5341c6SRobert Weinmeister            case 'remote100':
1216fcac025SRobert Weinmeister                $versions = array(
1226fcac025SRobert Weinmeister                    'latest' => '',
123a788b843SRobert Weinmeister                    'remote1091' => '@10.9.1',
1246fcac025SRobert Weinmeister                    'remote108' => '@10.8.0',
1256fcac025SRobert Weinmeister                    'remote106' => '@10.6.1',
1266fcac025SRobert Weinmeister                    'remote104' => '@10.4.0',
1276fcac025SRobert Weinmeister                    'remote103' => '@10.3.1',
1286fcac025SRobert Weinmeister                    'remote102' => '@10.2.4',
1296fcac025SRobert Weinmeister                    'remote101' => '@10.1.0',
1306fcac025SRobert Weinmeister                    'remote100' => '@10.0.2'
1316fcac025SRobert Weinmeister                );
1326fcac025SRobert Weinmeister                $data = "import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid".$versions[$location]."/dist/mermaid.esm.min.mjs';".$init;
1336e5341c6SRobert Weinmeister                $event->data['script'][] = array
1346e5341c6SRobert Weinmeister                (
1356e5341c6SRobert Weinmeister                    'type'    => 'module',
1366e5341c6SRobert Weinmeister                    'charset' => 'utf-8',
1376fcac025SRobert Weinmeister                    '_data' => $data
1386e5341c6SRobert Weinmeister                );
1396e5341c6SRobert Weinmeister                break;
140a788b843SRobert Weinmeister            // option remote94 is depreciated and only included for backward compatibility
1416e5341c6SRobert Weinmeister            case 'remote94':
142a788b843SRobert Weinmeister            case 'remote943':
1436e5341c6SRobert Weinmeister                $event->data['script'][] = array
1446e5341c6SRobert Weinmeister                (
1456e5341c6SRobert Weinmeister                    'type'    => 'text/javascript',
1466e5341c6SRobert Weinmeister                    'charset' => 'utf-8',
1474df3d176SRobert Weinmeister                    'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.4.3/dist/mermaid.min.js'
1486e5341c6SRobert Weinmeister                );
1496e5341c6SRobert Weinmeister                break;
150a788b843SRobert Weinmeister            // option remote93 is depreciated and only included for backward compatibility
1512d4b7fc2SRobert Weinmeister            case 'remote93':
1522d4b7fc2SRobert Weinmeister                $event->data['script'][] = array
1532d4b7fc2SRobert Weinmeister                (
1542d4b7fc2SRobert Weinmeister                    'type'    => 'text/javascript',
1552d4b7fc2SRobert Weinmeister                    'charset' => 'utf-8',
1564df3d176SRobert Weinmeister                    'src' => 'https://cdn.jsdelivr.net/npm/mermaid@9.3.0/dist/mermaid.min.js'
1572d4b7fc2SRobert Weinmeister                );
1582d4b7fc2SRobert Weinmeister                break;
1592d4b7fc2SRobert Weinmeister            default:
1602d4b7fc2SRobert Weinmeister        }
16146a60b4fSRobertWeinmeister
16246a60b4fSRobertWeinmeister        $event->data['link'][] = array
16346a60b4fSRobertWeinmeister        (
16446a60b4fSRobertWeinmeister            'rel'     => 'stylesheet',
16546a60b4fSRobertWeinmeister            'type'    => 'text/css',
16646a60b4fSRobertWeinmeister            'href'    => DOKU_BASE."lib/plugins/mermaid/mermaid.css",
16746a60b4fSRobertWeinmeister        );
16846a60b4fSRobertWeinmeister
1696fcac025SRobert Weinmeister        switch ($location) {
1704df3d176SRobert Weinmeister            case 'local':
171a788b843SRobert Weinmeister            case 'remote943':
172a788b843SRobert Weinmeister            // options remote94 and remote93 are depreciated and only included for backward compatibility
1736e5341c6SRobert Weinmeister            case 'remote94':
1746e5341c6SRobert Weinmeister            case 'remote93':
17546a60b4fSRobertWeinmeister                $event->data['script'][] = array
17646a60b4fSRobertWeinmeister                (
17746a60b4fSRobertWeinmeister                    'type'    => 'text/javascript',
17846a60b4fSRobertWeinmeister                    'charset' => 'utf-8',
1794c8bd9ffSRobert Weinmeister                    '_data'   => $init
18046a60b4fSRobertWeinmeister                );
1816e5341c6SRobert Weinmeister                break;
1826e5341c6SRobert Weinmeister            default:
1836e5341c6SRobert Weinmeister        }
1846fcac025SRobert Weinmeister
1856fcac025SRobert Weinmeister        // remove the search highlight from DokuWiki as it interferes with the Mermaid parsing/rendering
1866fcac025SRobert Weinmeister        $event->data['script'][] = array
1876fcac025SRobert Weinmeister        (
1886fcac025SRobert Weinmeister            'type'    => 'text/javascript',
1896fcac025SRobert Weinmeister            'charset' => 'utf-8',
1906fcac025SRobert Weinmeister            '_data' => "document.addEventListener('DOMContentLoaded', function() {
1916fcac025SRobert Weinmeister                            jQuery('.mermaid').each(function() {
1926fcac025SRobert Weinmeister                                var modifiedContent = jQuery(this).html().replace(/<span class=\"search_hit\">(.+?)<\/span>/g, '$1');
1936fcac025SRobert Weinmeister                                jQuery(this).html(modifiedContent);
1946fcac025SRobert Weinmeister                             })
1956fcac025SRobert Weinmeister                        });"
1966fcac025SRobert Weinmeister        );
1971da12d6eSRobert Weinmeister
1981da12d6eSRobert Weinmeister        // adds image-save capability
1991da12d6eSRobert Weinmeister        // First: Wait until the DOM content is fully loaded
2001da12d6eSRobert Weinmeister        // Second: Wait until Mermaid has changed the dokuwiki content to an svg
2011da12d6eSRobert Weinmeister        $event->data['script'][] = array
2021da12d6eSRobert Weinmeister        (
2031da12d6eSRobert Weinmeister            'type'    => 'text/javascript',
2041da12d6eSRobert Weinmeister            'charset' => 'utf-8',
2051da12d6eSRobert Weinmeister            '_data' => "
2061da12d6eSRobert Weinmeisterdocument.addEventListener('DOMContentLoaded', function() {
2071da12d6eSRobert Weinmeister     var config = {
2081da12d6eSRobert Weinmeister        childList: true,
2091da12d6eSRobert Weinmeister        subtree: true,
2101da12d6eSRobert Weinmeister        characterData: true
2111da12d6eSRobert Weinmeister    };
2121da12d6eSRobert Weinmeister
213172fa282SRobert Weinmeister    function callDokuWikiPHP(mode, index, mermaidRaw, mermaidSvg) {
214172fa282SRobert Weinmeister    jQuery.post(
215172fa282SRobert Weinmeister        DOKU_BASE + 'lib/exe/ajax.php',
216172fa282SRobert Weinmeister        {
217172fa282SRobert Weinmeister            call: 'plugin_mermaid',
218172fa282SRobert Weinmeister            mode: mode,
219172fa282SRobert Weinmeister            mermaidindex: index,
220172fa282SRobert Weinmeister            pageid: '".getID()."',
221172fa282SRobert Weinmeister            svg: encodeURIComponent(mermaidSvg)
222172fa282SRobert Weinmeister        },
223172fa282SRobert Weinmeister        function(response) {
224172fa282SRobert Weinmeister            if(response.status == 'success') {
225172fa282SRobert Weinmeister                location.reload(true);
226172fa282SRobert Weinmeister            }
227172fa282SRobert Weinmeister            else {
228172fa282SRobert Weinmeister                alert(response.data[0]);
229172fa282SRobert Weinmeister            }
230172fa282SRobert Weinmeister        },
231172fa282SRobert Weinmeister        'json'
232172fa282SRobert Weinmeister    )};
233172fa282SRobert Weinmeister
234172fa282SRobert Weinmeister    jQuery('.mermaidlocked, .mermaid').each(function(index, element) {
2351da12d6eSRobert Weinmeister        document.getElementById('mermaidContainer' + index).addEventListener('mouseenter', function() {
236172fa282SRobert Weinmeister             document.getElementById('mermaidFieldset' + index).style.display = 'flex';
2371da12d6eSRobert Weinmeister        });
2381da12d6eSRobert Weinmeister        document.getElementById('mermaidContainer' + index).addEventListener('mouseleave', function() {
239172fa282SRobert Weinmeister            document.getElementById('mermaidFieldset' + index).style.display = 'none';
2401da12d6eSRobert Weinmeister        });
2411da12d6eSRobert Weinmeister
242172fa282SRobert Weinmeister        if(jQuery(element).hasClass('mermaidlocked')) {
243172fa282SRobert Weinmeister            document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => {
2441da12d6eSRobert Weinmeister                var svgContent = element.innerHTML.trim();
2451da12d6eSRobert Weinmeister                var blob = new Blob([svgContent], { type: 'image/svg+xml' });
2461da12d6eSRobert Weinmeister                var link = document.createElement('a');
2471da12d6eSRobert Weinmeister                link.href = URL.createObjectURL(blob);
2481da12d6eSRobert Weinmeister                link.download = 'mermaid' + index + '.svg';
2491da12d6eSRobert Weinmeister                link.click();
2501da12d6eSRobert Weinmeister                URL.revokeObjectURL(link.href);
2511da12d6eSRobert Weinmeister            });
252172fa282SRobert Weinmeister
253172fa282SRobert Weinmeister            document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => {
254172fa282SRobert Weinmeister                if(confirm('Unlock Mermaid diagram?')) {
255172fa282SRobert Weinmeister                    callDokuWikiPHP('unlock', index, originalMermaidContent, element.innerHTML.trim());
256172fa282SRobert Weinmeister                }
257172fa282SRobert Weinmeister            });
258172fa282SRobert Weinmeister        }
259172fa282SRobert Weinmeister
260172fa282SRobert Weinmeister        if(jQuery(element).hasClass('mermaid')) {
261172fa282SRobert Weinmeister            var originalMermaidContent = element.innerHTML;
262172fa282SRobert Weinmeister            var observer = new MutationObserver(function(mutations) {
263172fa282SRobert Weinmeister                mutations.forEach(function(mutation) {
264172fa282SRobert Weinmeister                    if (mutation.type === 'childList' && element.innerHTML.startsWith('<svg')) {
265172fa282SRobert Weinmeister                        document.getElementById('mermaidButtonSave' + index).addEventListener('click', () => {
266172fa282SRobert Weinmeister                            var svgContent = element.innerHTML.trim();
267172fa282SRobert Weinmeister                            var blob = new Blob([svgContent], { type: 'image/svg+xml' });
268172fa282SRobert Weinmeister                            var link = document.createElement('a');
269172fa282SRobert Weinmeister                            link.href = URL.createObjectURL(blob);
270172fa282SRobert Weinmeister                            link.download = 'mermaid' + index + '.svg';
271172fa282SRobert Weinmeister                            link.click();
272172fa282SRobert Weinmeister                            URL.revokeObjectURL(link.href);
273172fa282SRobert Weinmeister                        });
274172fa282SRobert Weinmeister
275172fa282SRobert Weinmeister                       document.getElementById('mermaidButtonPermanent' + index).addEventListener('click', () => {
276172fa282SRobert Weinmeister                            if(confirm('Lock Mermaid diagram? [experimental]')) {
277172fa282SRobert Weinmeister                                callDokuWikiPHP('lock', index, originalMermaidContent, element.innerHTML.trim());
278172fa282SRobert Weinmeister                            }
279172fa282SRobert Weinmeister                       });
2801da12d6eSRobert Weinmeister                    }
2811da12d6eSRobert Weinmeister                });
2821da12d6eSRobert Weinmeister            });
2831da12d6eSRobert Weinmeister            observer.observe(element, config);
284172fa282SRobert Weinmeister        }
2851da12d6eSRobert Weinmeister    });
2861da12d6eSRobert Weinmeister});"
2871da12d6eSRobert Weinmeister        );
28846a60b4fSRobertWeinmeister    }
28946a60b4fSRobertWeinmeister}
2901da12d6eSRobert Weinmeister
291