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