1<?php 2/** 3 * DokuWiki Plugin geogebrembed (Syntax Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Philipp Imhof <dev@imhof.cc> 7 */ 8class syntax_plugin_geogebrembed_ggb extends \dokuwiki\Extension\SyntaxPlugin { 9 // track whether we have already imported GeoGebra's deployggb.js 10 private $import_done = false; 11 12 // count GeoGebra applets on any given page 13 private $count = 0; 14 15 // each applet gets its own set of parameters 16 private $params = ''; 17 18 // track whether we have already imported GeoGebra's deployggb.js 19 private $center = ''; 20 21 // each applet can have custom additional CSS classes 22 private $css_classes = ''; 23 24 /** @inheritDoc */ 25 public function getType() { 26 return 'protected'; 27 } 28 29 /** @inheritDoc */ 30 public function getPType() { 31 return 'block'; 32 } 33 34 /** @inheritDoc */ 35 public function getSort() { 36 return 200; 37 } 38 39 /** @inheritDoc */ 40 public function connectTo($mode) { 41 $this->Lexer->addEntryPattern('< ?ggb(?!ref|caption).*?>(?=.*?</ggb>)', $mode, 'plugin_geogebrembed_ggb'); 42 } 43 44 /** @inheritDoc */ 45 public function postConnect() { 46 $this->Lexer->addExitPattern('</ggb>', 'plugin_geogebrembed_ggb'); 47 } 48 49 /** @inheritDoc */ 50 public function handle($match, $state, $pos, Doku_Handler $handler) { 51 switch ($state) { 52 case DOKU_LEXER_ENTER : 53 // check whether the applet should be centered 54 if (substr($match, 0, 2) == '< ') { 55 $this->center = 'center'; 56 $match = str_replace('< ggb', '<ggb', $match); 57 } 58 else { 59 $this->center = ''; 60 } 61 62 // replace short form parameters by their official syntax 63 $substitutions = array( 64 '/\bfsb\b/' => 'showFullscreenButton=true', 65 '/\bnofsb\b/' => 'showFullscreenButton=false', 66 '/\brc\b/' => 'enableRightClick=true', 67 '/\bnorc\b/' => 'enableRightClick=false', 68 '/\bld\b/' => 'enableLabelDrags=true', 69 '/\bnold\b/' => 'enableLabelDrags=false', 70 '/\bsdz\b/' => 'enableShiftDragZoom=true', 71 '/\bnosdz\b/' => 'enableShiftDragZoom=false', 72 '/\bzb\b/' => 'showZoomButtons=true', 73 '/\bnozb\b/' => 'showZoomButtons=false', 74 '/\bab\b/' => 'showAnimationButton=true', 75 '/\bnoab\b/' => 'showAnimationButton=false', 76 '/\bmb\b/' => 'showMenuBar=true', 77 '/\bnomb\b/' => 'showMenuBar=false', 78 '/\btb\b/' => 'showToolBar=true', 79 '/\bnotb\b/' => 'showToolBar=false', 80 '/\bri\b/' => 'showResetIcon=true', 81 '/\bnori\b/' => 'showResetIcon=false', 82 '/\bai\b/' => 'showAlgebraInput=true', 83 '/\bnoai\b/' => 'showAlgebraInput=false', 84 '/\bsb\b/' => 'allowStyleBar=true', 85 '/\bnosb\b/' => 'allowStyleBar=false', 86 '/\bpb\b/' => 'playButton=true', 87 '/\bnopb\b/' => 'playButton=false', 88 '/\bborder\b/' => 'borderColor', 89 '/\bbc\b/' => 'borderColor' 90 ); 91 $params_raw = preg_replace(array_keys($substitutions), array_values($substitutions), $match); 92 93 // search for optional CSS classes 94 $regex = '/class=(["\'])([^\1]+?)\1/'; 95 if (preg_match($regex, $params_raw, $content)) { 96 $this->css_classes = $content[2]; 97 $params_raw = preg_replace($regex, '', $params_raw); 98 } 99 else { 100 $this->css_classes = ''; 101 } 102 103 // split params at whitespace 104 $params = preg_split('/\s/', substr($params_raw, 4, -1), -1, PREG_SPLIT_NO_EMPTY); 105 106 // size, if specified in its short form, must be the first parameter 107 // e.g. 400 (explicit width, auto height) or 400x300 (width x height) 108 $size = array(); 109 if (preg_match('/^(\d+)(?:x(\d+))?$/', $params[0], $size)) { 110 $params[0] = "width=$size[1]"; 111 if (count($size)==3) { 112 $params[0] .= ", height=$size[2], autoHeight=false"; 113 } 114 else { 115 $params[0] .= ', autoHeight=true'; 116 } 117 } 118 119 // store parameter string 120 $this->params = implode(', ', str_replace('=', ': ', $params)); 121 return array($state, array('center' => $this->center, 122 'css_classes' => $this->css_classes, 'params' => $this->params)); 123 124 case DOKU_LEXER_UNMATCHED : 125 if (substr($match, 0, 2) == '{{') { 126 $path = ml(preg_replace('/^\{\{([^|]+).*\}\}$/', '\1', $match)); 127 $this->params .= ", filename: \"$path\""; 128 } 129 // force interpretation as GeoGebra material ID 130 else if (substr($match, 0, 3) == 'id:') { 131 $material_id = substr($match, 3); 132 $this->params .= ", material_id: \"$material_id\""; 133 } 134 else if (preg_match('/^[A-Z0-9]{0,'.$this->getConf('config_threshold').'}$/i', $match)) { 135 $material_id = $match; 136 $this->params .= ", material_id: \"$material_id\""; 137 } 138 else { 139 if (base64_decode($match, true)) { 140 $this->params .= ", ggbBase64: \"$match\""; 141 } 142 } 143 return array($state, array('center' => $this->center, 144 'css_classes' => $this->css_classes, 'params' => $this->params)); 145 146 case DOKU_LEXER_EXIT : 147 // load configuration, if needed 148 if (!$this->configloaded) { 149 $this->loadConfig(); 150 } 151 152 // find unset parameters that have a pre-configured default value 153 $default_settings = str_replace('default_', '', array_keys($this->conf)); 154 $current_settings = $this->params; 155 foreach ($default_settings as $s) { 156 // if the parameter is already set or if its name contains an underscore: discard it 157 if (strstr($current_settings, $s) or strstr($s, '_')) continue; 158 159 // do not set height if autoHeight is set to true 160 if ($s == "height" and strstr($current_settings, 'autoHeight')) continue; 161 162 // do not set width and height, if scaleContainerClass is used 163 if ($s == "width" and strstr($current_settings, 'scaleContainerClass')) continue; 164 if ($s == "height" and strstr($current_settings, 'scaleContainerClass')) continue; 165 166 $current_settings .= ", $s: "; 167 $val = $this->conf["default_$s"]; 168 switch (gettype($val)) { 169 case "string": 170 $current_settings .= '"'.$val.'"'; 171 break; 172 case "integer": 173 if ($val === 0) { 174 $current_settings .= 'false'; 175 } 176 else if ($val === 1) { 177 $current_settings .= 'true'; 178 } 179 else { 180 $current_settings .= $val; 181 } 182 } 183 } 184 185 $this->params = trim($current_settings, ' ,'); 186 return array($state, array('center' => $this->center, 187 'css_classes' => $this->css_classes, 'params' => $this->params)); 188 189 default: 190 return array(); 191 } 192 } 193 194 /** @inheritDoc */ 195 public function render($mode, Doku_Renderer $renderer, $data) { 196 if ($mode !== 'xhtml') { 197 return false; 198 } 199 200 // for first invocation: import deployment script and reset counter to zero 201 if (!$this->import_done) { 202 $url = $this->getConf('config_url'); 203 $renderer->doc .= "<script src=\"$url\" async></script>"; 204 $this->import_done = true; 205 $this->count = 0; 206 return true; 207 } 208 209 // end of current GeoGebra applet: add div and inject applet 210 if ($data[0] === DOKU_LEXER_EXIT) { 211 $renderer->doc .= <<<GGB 212<div class="geogebrembed-wrapper {$data[1]['center']}"> 213<div id="ggb-$this->count" class="geogebrembed {$data[1]['css_classes']}"></div> 214</div> 215<script> 216 window.addEventListener('load', () => { 217 let import_$this->count = new GGBApplet({ 218 {$data[1]['params']} 219 }, true); 220 import_$this->count.inject("ggb-$this->count"); 221 }); 222</script> 223GGB; 224 225 // step the counter 226 $this->count++; 227 return true; 228 } 229 230 return true; 231 } 232} 233 234