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 9b566ae41SRobert Weinmeisterdeclare(strict_types=1); 10b566ae41SRobert Weinmeister 11b566ae41SRobert Weinmeisterif (!defined('DOKU_INC')) { 12b566ae41SRobert Weinmeister die(); 13b566ae41SRobert Weinmeister} 14ea08b541SRobert Weinmeister 1546a60b4fSRobertWeinmeisterclass action_plugin_mermaid extends \dokuwiki\Extension\ActionPlugin 1646a60b4fSRobertWeinmeister{ 1746a60b4fSRobertWeinmeister /** @inheritDoc */ 18b566ae41SRobert Weinmeister public function register(Doku_Event_Handler $controller): void { 1946a60b4fSRobertWeinmeister $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'load'); 20172fa282SRobert Weinmeister $controller->register_hook('AJAX_CALL_UNKNOWN', 'BEFORE', $this, 'handleAjaxRequest'); 21172fa282SRobert Weinmeister } 22172fa282SRobert Weinmeister 23b566ae41SRobert Weinmeister private function hasPermissionToEdit(string $ID): bool { 24b566ae41SRobert Weinmeister return auth_quickaclcheck($ID) >= AUTH_EDIT; 25172fa282SRobert Weinmeister } 26172fa282SRobert Weinmeister 27b566ae41SRobert Weinmeister private function isPageLocked(string $ID): bool { 28b566ae41SRobert Weinmeister return checklock($ID); 29b566ae41SRobert Weinmeister } 30b566ae41SRobert Weinmeister 31b566ae41SRobert Weinmeister private function lockMermaidDiagram(string $wikitext): string { 32b566ae41SRobert Weinmeister preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE); 33b566ae41SRobert Weinmeister 34b566ae41SRobert Weinmeister if (is_array($matches) && count($matches[0]) > (int)$_REQUEST['mermaidindex']) { 35b566ae41SRobert Weinmeister $whereToInsert = $matches[1][(int)$_REQUEST['mermaidindex']][1]; 36b566ae41SRobert Weinmeister return substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert); 37b566ae41SRobert Weinmeister } 38b566ae41SRobert Weinmeister 39b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]); 40b566ae41SRobert Weinmeister exit(); 41b566ae41SRobert Weinmeister } 42b566ae41SRobert Weinmeister 43b566ae41SRobert Weinmeister private function unlockMermaidDiagram(string $wikitext): string { 44b566ae41SRobert Weinmeister $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count); 45b566ae41SRobert Weinmeister 46b566ae41SRobert Weinmeister if ($count !== 1) { 47b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]); 48b566ae41SRobert Weinmeister exit(); 49b566ae41SRobert Weinmeister } 50b566ae41SRobert Weinmeister 51b566ae41SRobert Weinmeister return $newWikitext; 52b566ae41SRobert Weinmeister } 53b566ae41SRobert Weinmeister 54b566ae41SRobert Weinmeister private function isWikiTextChanged(string $wikitext, string $newWikitext): bool { 55b566ae41SRobert Weinmeister return strlen($newWikitext) > 0 && $newWikitext !== $wikitext; 56b566ae41SRobert Weinmeister } 57b566ae41SRobert Weinmeister 58b566ae41SRobert Weinmeister private function saveWikiChanges(string $ID, string $newWikitext, string $mode): void { 59b566ae41SRobert Weinmeister lock($ID); 60b566ae41SRobert Weinmeister saveWikiText($ID, $newWikitext, "{$mode} Mermaid diagram", true); 61b566ae41SRobert Weinmeister unlock($ID); 62b566ae41SRobert Weinmeister } 63b566ae41SRobert Weinmeister 64b566ae41SRobert Weinmeister public function handleAjaxRequest(Doku_Event $event, $param): void { 65b566ae41SRobert Weinmeister if ($event->data !== 'plugin_mermaid') { 66b566ae41SRobert Weinmeister return; 67b566ae41SRobert Weinmeister } 68172fa282SRobert Weinmeister $event->stopPropagation(); 69172fa282SRobert Weinmeister $event->preventDefault(); 70172fa282SRobert Weinmeister 71b566ae41SRobert Weinmeister if (!isset($_REQUEST['mermaidindex']) || !isset($_REQUEST['svg'])) { 72b566ae41SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Missing required parameters.']]); 73b566ae41SRobert Weinmeister exit(); 74b566ae41SRobert Weinmeister } 75b566ae41SRobert Weinmeister 76172fa282SRobert Weinmeister $ID = cleanID(urldecode($_REQUEST['pageid'])); 77172fa282SRobert Weinmeister 78b566ae41SRobert Weinmeister if(!$this->hasPermissionToEdit($ID)) { 79172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['You do not have permission to edit this file.\nAccess was denied.']]); 80172fa282SRobert Weinmeister exit(); 81172fa282SRobert Weinmeister } 82172fa282SRobert Weinmeister 83b566ae41SRobert Weinmeister if($this->isPageLocked($ID)) { 84172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['The page is currently locked.\nTry again later.']]); 85172fa282SRobert Weinmeister exit(); 86172fa282SRobert Weinmeister } 87172fa282SRobert Weinmeister 88172fa282SRobert Weinmeister $wikitext = rawWiki($ID); 89172fa282SRobert Weinmeister $newWikitext = $wikitext; 90172fa282SRobert Weinmeister 91b566ae41SRobert Weinmeister if($_REQUEST['mode'] === 'lock') { 92172fa282SRobert Weinmeister preg_match_all('/<mermaid.*?>(.*?)<\/mermaid>/s', $wikitext, $matches, PREG_OFFSET_CAPTURE); 93172fa282SRobert Weinmeister 94172fa282SRobert Weinmeister if(is_array($matches) && count($matches[0]) > $_REQUEST['mermaidindex']) 95172fa282SRobert Weinmeister { 96172fa282SRobert Weinmeister $whereToInsert = $matches[1][$_REQUEST['mermaidindex']][1]; 97172fa282SRobert Weinmeister $newWikitext = substr($wikitext, 0, $whereToInsert) . "\n%%" . urldecode($_REQUEST['svg']) . "\n" . substr($wikitext, $whereToInsert); 98172fa282SRobert Weinmeister } 99172fa282SRobert Weinmeister else 100172fa282SRobert Weinmeister { 101172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not lock the Mermaid diagram as the request could not be matched.']]); 102172fa282SRobert Weinmeister exit(); 103172fa282SRobert Weinmeister } 104172fa282SRobert Weinmeister } 105172fa282SRobert Weinmeister 106172fa282SRobert Weinmeister if($_REQUEST['mode'] == 'unlock') 107172fa282SRobert Weinmeister { 108172fa282SRobert Weinmeister $newWikitext = str_replace("\n%%" . urldecode($_REQUEST['svg']) . "\n", '', $wikitext, $count); 109172fa282SRobert Weinmeister if($count != 1) 110172fa282SRobert Weinmeister { 111172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not unlock the Mermaid diagram as the request could not be matched.']]); 112172fa282SRobert Weinmeister exit(); 113172fa282SRobert Weinmeister } 114172fa282SRobert Weinmeister } 115172fa282SRobert Weinmeister 116b566ae41SRobert Weinmeister if($this->isWikiTextChanged($wikitext, $newWikitext)) { 117b566ae41SRobert Weinmeister $this->saveWikiChanges($ID, $newWikitext, $_REQUEST['mode']); 118172fa282SRobert Weinmeister echo json_encode(['status' => 'success', 'data' => []]); 119b566ae41SRobert Weinmeister } else{ 120172fa282SRobert Weinmeister echo json_encode(['status' => 'failure', 'data' => ['Could not ' . $_REQUEST['mode'] . ' the Mermaid diagram.']]); 121b566ae41SRobert Weinmeister } 122b566ae41SRobert Weinmeister 123172fa282SRobert Weinmeister exit(); 12446a60b4fSRobertWeinmeister } 12546a60b4fSRobertWeinmeister 126b566ae41SRobert Weinmeister private function addLocalScript(Doku_Event $event): void { 127b566ae41SRobert Weinmeister $event->data['script'][] = [ 128b566ae41SRobert Weinmeister 'type' => 'text/javascript', 129b566ae41SRobert Weinmeister 'charset' => 'utf-8', 130b566ae41SRobert Weinmeister 'src' => DOKU_BASE . 'lib/plugins/mermaid/mermaid.min.js', 131b566ae41SRobert Weinmeister ]; 132b566ae41SRobert Weinmeister } 133b566ae41SRobert Weinmeister 134b566ae41SRobert Weinmeister private function addEsmScript(Doku_Event $event, string $version, string $init): void { 135b566ae41SRobert Weinmeister $data = "import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid{$version}/dist/mermaid.esm.min.mjs';{$init}"; 136b566ae41SRobert Weinmeister $event->data['script'][] = [ 137b566ae41SRobert Weinmeister 'type' => 'module', 138b566ae41SRobert Weinmeister 'charset' => 'utf-8', 139b566ae41SRobert Weinmeister '_data' => $data, 140b566ae41SRobert Weinmeister ]; 141b566ae41SRobert Weinmeister } 142b566ae41SRobert Weinmeister 143b566ae41SRobert Weinmeister private function addScript(Doku_Event $event, string $version, string $init): void { 144b566ae41SRobert Weinmeister $event->data['script'][] = [ 145b566ae41SRobert Weinmeister 'type' => 'text/javascript', 146b566ae41SRobert Weinmeister 'charset' => 'utf-8', 147b566ae41SRobert Weinmeister 'src' => "https://cdn.jsdelivr.net/npm/mermaid{$version}/dist/mermaid.min.js", 148b566ae41SRobert Weinmeister ]; 149b566ae41SRobert Weinmeister 150b566ae41SRobert Weinmeister $event->data['script'][] = [ 151b566ae41SRobert Weinmeister 'type' => 'text/javascript', 152b566ae41SRobert Weinmeister 'charset' => 'utf-8', 153b566ae41SRobert Weinmeister '_data' => $init, 154b566ae41SRobert Weinmeister ]; 155b566ae41SRobert Weinmeister } 156b566ae41SRobert Weinmeister 157b74fbfffSRobert Weinmeister private function pageIncludesMermaid(): bool { 158b74fbfffSRobert Weinmeister // true if the mermaid tag is used 159b74fbfffSRobert Weinmeister // the include plugin can hide this fact, so we need a separate check for it 160*c769b866SMartin Lormes global $ACT; 161*c769b866SMartin Lormes global $TEXT; 162*c769b866SMartin Lormes if ('preview'==$ACT) { 163*c769b866SMartin Lormes $wikiText = $TEXT; 164*c769b866SMartin Lormes } else { 165b74fbfffSRobert Weinmeister $wikiText = rawWiki(getID()); 166*c769b866SMartin Lormes } 167*c769b866SMartin Lormes 168*c769b866SMartin Lormes if ('edit'==$ACT) { 169*c769b866SMartin Lormes return false; 170*c769b866SMartin Lormes } 171b74fbfffSRobert Weinmeister if (str_contains($wikiText, '<mermaid') || str_contains($wikiText, '{{page>') || str_contains($wikiText, '{{section>') || str_contains($wikiText, '{{namespace>') || str_contains($wikiText, '{{tagtopic>')) { 172b74fbfffSRobert Weinmeister return true; 173b74fbfffSRobert Weinmeister } 174b74fbfffSRobert Weinmeister 175b74fbfffSRobert Weinmeister return false; 176b74fbfffSRobert Weinmeister } 177b74fbfffSRobert Weinmeister 178b566ae41SRobert Weinmeister /** 179b566ae41SRobert Weinmeister * Load the Mermaid library and configuration into the page. 180b566ae41SRobert Weinmeister * 181b566ae41SRobert Weinmeister * @param Doku_Event $event DokuWiki event object 182b566ae41SRobert Weinmeister * @param mixed $param Unused parameter. 183b566ae41SRobert Weinmeister */ 184b566ae41SRobert Weinmeister public function load(Doku_Event $event, $param): void { 185ea08b541SRobert Weinmeister // only load mermaid if it is needed 186b74fbfffSRobert Weinmeister if (!$this->pageIncludesMermaid()) { 187ea08b541SRobert Weinmeister return; 188ea08b541SRobert Weinmeister } 189ea08b541SRobert Weinmeister 1904c8bd9ffSRobert Weinmeister $theme = $this->getConf('theme'); 19155e3db93SRobert Weinmeister $look = $this->getConf('look'); 19255e3db93SRobert Weinmeister $logLevel = $this->getConf('logLevel'); 193b566ae41SRobert Weinmeister $init = "mermaid.initialize({startOnLoad: true, logLevel: '$logLevel', theme: '$theme', look: '$look'});"; 1944c8bd9ffSRobert Weinmeister 195b566ae41SRobert Weinmeister $location = $this->getConf('location'); 196b566ae41SRobert Weinmeister $versions = [ 1976fcac025SRobert Weinmeister 'latest' => '', 19892788e2aSRobert Weinmeister 'remote1095' => '@10.9.5', 199a788b843SRobert Weinmeister 'remote1091' => '@10.9.1', 2006fcac025SRobert Weinmeister 'remote108' => '@10.8.0', 2016fcac025SRobert Weinmeister 'remote106' => '@10.6.1', 2026fcac025SRobert Weinmeister 'remote104' => '@10.4.0', 2036fcac025SRobert Weinmeister 'remote103' => '@10.3.1', 2046fcac025SRobert Weinmeister 'remote102' => '@10.2.4', 2056fcac025SRobert Weinmeister 'remote101' => '@10.1.0', 206b566ae41SRobert Weinmeister 'remote100' => '@10.0.2', 207b566ae41SRobert Weinmeister 'remote94' => '@9.4.3', 208b566ae41SRobert Weinmeister 'remote943' => '@9.4.3', 209b566ae41SRobert Weinmeister 'remote93' => '@9.3.0', 210b566ae41SRobert Weinmeister ]; 21146a60b4fSRobertWeinmeister 212b566ae41SRobert Weinmeister // add the appropriate Mermaid script based on the location configuration 213b566ae41SRobert Weinmeister match ($location) { 214b566ae41SRobert Weinmeister 'local' => $this->addLocalScript($event), 21592788e2aSRobert Weinmeister 'latest', 'remote1095', 'remote1091', 'remote108', 'remote106', 'remote104', 'remote103', 'remote102', 'remote101', 'remote100' 216b566ae41SRobert Weinmeister => $this->addEsmScript($event, $versions[$location], $init), 217b566ae41SRobert Weinmeister 'remote94', 'remote943', 'remote93' 218b566ae41SRobert Weinmeister => $this->addScript($event, $versions[$location], $init), 219b566ae41SRobert Weinmeister default => null, 220b566ae41SRobert Weinmeister }; 221b566ae41SRobert Weinmeister 222b566ae41SRobert Weinmeister $event->data['link'][] = [ 22346a60b4fSRobertWeinmeister 'rel' => 'stylesheet', 22446a60b4fSRobertWeinmeister 'type' => 'text/css', 22546a60b4fSRobertWeinmeister 'href' => DOKU_BASE . "lib/plugins/mermaid/mermaid.css", 226b566ae41SRobert Weinmeister ]; 2276fcac025SRobert Weinmeister 2286fcac025SRobert Weinmeister // remove the search highlight from DokuWiki as it interferes with the Mermaid parsing/rendering 229b566ae41SRobert Weinmeister $event->data['script'][] = [ 2306fcac025SRobert Weinmeister 'type' => 'text/javascript', 2316fcac025SRobert Weinmeister 'charset' => 'utf-8', 2326fcac025SRobert Weinmeister '_data' => "document.addEventListener('DOMContentLoaded', function() { 2336fcac025SRobert Weinmeister jQuery('.mermaid').each(function() { 2346fcac025SRobert Weinmeister var modifiedContent = jQuery(this).html().replace(/<span class=\"search_hit\">(.+?)<\/span>/g, '$1'); 2356fcac025SRobert Weinmeister jQuery(this).html(modifiedContent); 2366fcac025SRobert Weinmeister }) 2376fcac025SRobert Weinmeister });" 238b566ae41SRobert Weinmeister ]; 2391da12d6eSRobert Weinmeister 2401da12d6eSRobert Weinmeister // adds image-save capability 2411da12d6eSRobert Weinmeister // First: Wait until the DOM content is fully loaded 2421da12d6eSRobert Weinmeister // Second: Wait until Mermaid has changed the dokuwiki content to an svg 2431da12d6eSRobert Weinmeister $event->data['script'][] = array 2441da12d6eSRobert Weinmeister ( 2451da12d6eSRobert Weinmeister 'type' => 'text/javascript', 2461da12d6eSRobert Weinmeister 'charset' => 'utf-8', 2471da12d6eSRobert Weinmeister '_data' => " 2481da12d6eSRobert Weinmeisterdocument.addEventListener('DOMContentLoaded', function() { 2491da12d6eSRobert Weinmeister var config = { 2501da12d6eSRobert Weinmeister childList: true, 2511da12d6eSRobert Weinmeister subtree: true, 2521da12d6eSRobert Weinmeister characterData: true 2531da12d6eSRobert Weinmeister }; 2541da12d6eSRobert Weinmeister 255172fa282SRobert Weinmeister function callDokuWikiPHP(mode, index, mermaidRaw, mermaidSvg) { 256172fa282SRobert Weinmeister jQuery.post( 257172fa282SRobert Weinmeister DOKU_BASE + 'lib/exe/ajax.php', 258172fa282SRobert Weinmeister { 259172fa282SRobert Weinmeister call: 'plugin_mermaid', 260172fa282SRobert Weinmeister mode: mode, 261172fa282SRobert Weinmeister mermaidindex: index, 262172fa282SRobert Weinmeister pageid: '".getID()."', 263172fa282SRobert Weinmeister svg: encodeURIComponent(mermaidSvg) 264172fa282SRobert Weinmeister }, 265172fa282SRobert Weinmeister function(response) { 266172fa282SRobert Weinmeister if(response.status == 'success') { 267172fa282SRobert Weinmeister location.reload(true); 268172fa282SRobert Weinmeister } 269172fa282SRobert Weinmeister else { 270172fa282SRobert Weinmeister alert(response.data[0]); 271172fa282SRobert Weinmeister } 272172fa282SRobert Weinmeister }, 273172fa282SRobert Weinmeister 'json' 274172fa282SRobert Weinmeister )}; 275172fa282SRobert Weinmeister 276172fa282SRobert Weinmeister jQuery('.mermaidlocked, .mermaid').each(function(index, element) { 2771da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseenter', function() { 2780ab8bda3Sdjh var fieldset = document.getElementById('mermaidFieldset' + index); 2790ab8bda3Sdjh if (fieldset) { 2800ab8bda3Sdjh fieldset.style.display = 'flex'; 2810ab8bda3Sdjh } 2821da12d6eSRobert Weinmeister }); 2831da12d6eSRobert Weinmeister document.getElementById('mermaidContainer' + index).addEventListener('mouseleave', function() { 2840ab8bda3Sdjh var fieldset = document.getElementById('mermaidFieldset' + index); 2850ab8bda3Sdjh if (fieldset) { 2860ab8bda3Sdjh fieldset.style.display = 'none'; 2870ab8bda3Sdjh } 2881da12d6eSRobert Weinmeister }); 2891da12d6eSRobert Weinmeister 290172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaidlocked')) { 2910ab8bda3Sdjh var buttonSave = document.getElementById('mermaidButtonSave' + index); 2920ab8bda3Sdjh if (buttonSave) { 2930ab8bda3Sdjh buttonSave.addEventListener('click', () => { 2941da12d6eSRobert Weinmeister var svgContent = element.innerHTML.trim(); 2951da12d6eSRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 2961da12d6eSRobert Weinmeister var link = document.createElement('a'); 2971da12d6eSRobert Weinmeister link.href = URL.createObjectURL(blob); 2981da12d6eSRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 2991da12d6eSRobert Weinmeister link.click(); 3001da12d6eSRobert Weinmeister URL.revokeObjectURL(link.href); 3011da12d6eSRobert Weinmeister }); 3020ab8bda3Sdjh } 3030ab8bda3Sdjh var buttonPermanent = document.getElementById('mermaidButtonPermanent' + index); 3040ab8bda3Sdjh if (buttonPermanent) { 3050ab8bda3Sdjh buttonPermanent.addEventListener('click', () => { 306172fa282SRobert Weinmeister if(confirm('Unlock Mermaid diagram?')) { 307172fa282SRobert Weinmeister callDokuWikiPHP('unlock', index, originalMermaidContent, element.innerHTML.trim()); 308172fa282SRobert Weinmeister } 309172fa282SRobert Weinmeister }); 310172fa282SRobert Weinmeister } 3110ab8bda3Sdjh } 312172fa282SRobert Weinmeister 313172fa282SRobert Weinmeister if(jQuery(element).hasClass('mermaid')) { 314172fa282SRobert Weinmeister var originalMermaidContent = element.innerHTML; 315172fa282SRobert Weinmeister var observer = new MutationObserver(function(mutations) { 316172fa282SRobert Weinmeister mutations.forEach(function(mutation) { 317172fa282SRobert Weinmeister if (mutation.type === 'childList' && element.innerHTML.startsWith('<svg')) { 3180ab8bda3Sdjh var saveButton = document.getElementById('mermaidButtonSave' + index); 3190ab8bda3Sdjh if (saveButton) { 3200ab8bda3Sdjh saveButton.addEventListener('click', () => { 321172fa282SRobert Weinmeister var svgContent = element.innerHTML.trim(); 322172fa282SRobert Weinmeister var blob = new Blob([svgContent], { type: 'image/svg+xml' }); 323172fa282SRobert Weinmeister var link = document.createElement('a'); 324172fa282SRobert Weinmeister link.href = URL.createObjectURL(blob); 325172fa282SRobert Weinmeister link.download = 'mermaid' + index + '.svg'; 326172fa282SRobert Weinmeister link.click(); 327172fa282SRobert Weinmeister URL.revokeObjectURL(link.href); 328172fa282SRobert Weinmeister }); 3290ab8bda3Sdjh } 3300ab8bda3Sdjh var buttonPermanent = document.getElementById('mermaidButtonPermanent' + index); 3310ab8bda3Sdjh if (buttonPermanent) { 3320ab8bda3Sdjh buttonPermanent.addEventListener('click', () => { 333172fa282SRobert Weinmeister if(confirm('Lock Mermaid diagram? [experimental]')) { 334172fa282SRobert Weinmeister callDokuWikiPHP('lock', index, originalMermaidContent, element.innerHTML.trim()); 335172fa282SRobert Weinmeister } 336172fa282SRobert Weinmeister }); 3371da12d6eSRobert Weinmeister } 3380ab8bda3Sdjh } 3391da12d6eSRobert Weinmeister }); 3401da12d6eSRobert Weinmeister }); 3411da12d6eSRobert Weinmeister observer.observe(element, config); 342172fa282SRobert Weinmeister } 3431da12d6eSRobert Weinmeister }); 3441da12d6eSRobert Weinmeister});" 3451da12d6eSRobert Weinmeister ); 34646a60b4fSRobertWeinmeister } 34746a60b4fSRobertWeinmeister} 348