xref: /plugin/ditaa/syntax.php (revision 967610bbd42ffed4044a679e2d0ed0ab200de40b)
1<?php
2/**
3 * Ditaa-Plugin: Converts Ascii-Flowcharts into a png-File
4 *
5 * @license     GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author      Dennis Ploeger <develop [at] dieploegers [dot] de>
7 * @author      Christoph Mertins <c [dot] mertins [at] gmail [dot] com>
8 * @author      Gerry Weißbach / i-net software <tools [at] inetsoftware [dot] de>
9 * @author      Christian Marg <marg@rz.tu-clausthal.de>
10 * @author      Andreas Gohr <andi@splitbrain.org>
11 */
12
13if(!defined('DOKU_INC')) define('DOKU_INC', realpath(dirname(__FILE__) . '/../../') . '/');
14if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
15require_once(DOKU_PLUGIN . 'syntax.php');
16
17class syntax_plugin_ditaa extends DokuWiki_Syntax_Plugin {
18
19    /**
20     * What about paragraphs?
21     */
22    function getPType() {
23        return 'normal';
24    }
25
26    /**
27     * What kind of syntax are we?
28     */
29    function getType() {
30        return 'substition';
31    }
32
33    /**
34     * Where to sort in?
35     */
36    function getSort() {
37        return 200;
38    }
39
40    /**
41     * Connect pattern to lexer
42     */
43
44    function connectTo($mode) {
45        $this->Lexer->addSpecialPattern('<ditaa.*?>\n.*?\n</ditaa>', $mode, 'plugin_ditaa');
46    }
47
48    /**
49     * Handle the match
50     */
51    function handle($match, $state, $pos, Doku_Handler $handler) {
52        $info = $this->getInfo();
53
54        // prepare default data
55        $return = array(
56            'width' => 0,
57            'height' => 0,
58            'antialias' => true,
59            'edgesep' => true,
60            'round' => false,
61            'shadow' => true,
62            'scale' => 1,
63            'align' => '',
64            'version' => $info['date'], //force rebuild of images on update
65        );
66
67        // prepare input
68        $lines = explode("\n", $match);
69        $conf = array_shift($lines);
70        array_pop($lines);
71
72        // match config options
73        if(preg_match('/\b(left|center|right)\b/i', $conf, $match)) $return['align'] = $match[1];
74        if(preg_match('/\b(\d+)x(\d+)\b/', $conf, $match)) {
75            $return['width'] = $match[1];
76            $return['height'] = $match[2];
77        }
78        if(preg_match('/\b(\d+(\.\d+)?)X\b/', $conf, $match)) $return['scale'] = $match[1];
79        if(preg_match('/\bwidth=([0-9]+)\b/i', $conf, $match)) $return['width'] = $match[1];
80        if(preg_match('/\bheight=([0-9]+)\b/i', $conf, $match)) $return['height'] = $match[1];
81        // match boolean toggles
82        if(preg_match_all('/\b(no)?(antialias|edgesep|round|shadow)\b/i', $conf, $matches, PREG_SET_ORDER)) {
83            foreach($matches as $match) {
84                $return[$match[2]] = !$match[1];
85            }
86        }
87
88        $input = join("\n", $lines);
89        $return['md5'] = md5($input); // we only pass a hash around
90
91        // store input for later use
92        io_saveFile($this->_cachename($return, 'txt'), $input);
93
94        return $return;
95    }
96
97    /**
98     * Prepares the Data that is used for the cache name
99     * Width, height and scale are left out.
100     * Ensures sanity.
101     */
102    function _prepareData($input) {
103        $output = array();
104        foreach($input as $key => $value) {
105            switch($key) {
106                case 'scale':
107                case 'antialias':
108                case 'edgesep':
109                case 'round':
110                case 'shadow':
111                case 'md5':
112                    $output[$key] = $value;
113            };
114        }
115
116        ksort($output);
117        return $output;
118    }
119
120    /**
121     * Cache file is based on parameters that influence the result image
122     */
123    function _cachename($data, $ext) {
124        $data = $this->_prepareData($data);
125        return getcachename(join('x', array_values($data)), '.ditaa.' . $ext);
126    }
127
128    /**
129     * Create output
130     */
131    function render($format, Doku_Renderer $R, $data) {
132        global $ID;
133        if($format == 'xhtml') {
134
135            // Only use the md5 key
136            $img = ml($ID, array('ditaa' => $data['md5']));
137            $R->doc .= '<img src="' . $img . '" class="media' . $data['align'] . '" alt=""';
138            if($data['width']) $R->doc .= ' width="' . $data['width'] . '"';
139            if($data['height']) $R->doc .= ' height="' . $data['height'] . '"';
140            if($data['align'] == 'right') $R->doc .= ' align="right"';
141            if($data['align'] == 'left') $R->doc .= ' align="left"';
142            $R->doc .= '/>';
143            return true;
144        } else if($format == 'odt') {
145            $src = $this->_imgfile($data);
146            $R->_odtAddImage($src, $data['width'], $data['height'], $data['align']);
147            return true;
148        } else if($format == 'metadata') {
149            // Save for later use
150            $R->meta['ditaa'][$data['md5']] = $data;
151            return true;
152        }
153        return false;
154    }
155
156    /**
157     * Return path to the rendered image on our local system
158     */
159    function _imgfile($id, $data, $secondTry = false) {
160
161        $cache = $this->_cachename($data, 'png');
162
163        // create the file if needed
164        if(!file_exists($cache)) {
165            $in = $this->_cachename($data, 'txt');
166            // If this is nt yet here, force geting instructions and writing the thing back.
167            if($secondTry != true && !file_exists($in)) {
168                p_get_instructions(io_readFile(wikiFN($id)));
169                return $this->_imgfile($id, $data, true);
170            }
171
172            if($this->getConf('java')) {
173                $ok = $this->_run($data, $in, $cache);
174            } else {
175                $ok = $this->_runGo($data, $in, $cache);
176                #$ok = $this->_remote($data, $in, $cache);
177            }
178            if(!$ok) return false;
179            clearstatcache();
180        }
181
182        // resized version
183        if($data['width']) {
184            $cache = media_resize_image($cache, 'png', $data['width'], $data['height']);
185        }
186
187        // something went wrong, we're missing the file
188        if(!file_exists($cache)) return false;
189
190        return $cache;
191    }
192
193    /**
194     * Render the output remotely at ditaa.org
195     */
196    function _remote($data, $in, $out) {
197        global $conf;
198
199        if(!file_exists($in)) {
200            if($conf['debug']) {
201                dbglog($in, 'no such ditaa input file');
202            }
203            return false;
204        }
205
206        $http = new DokuHTTPClient();
207        $http->timeout = 30;
208
209        $pass = array();
210        $pass['scale'] = $data['scale'];
211        $pass['timeout'] = 25;
212        $pass['grid'] = io_readFile($in);
213        if(!$data['antialias']) $pass['A'] = 'on';
214        if(!$data['shadow']) $pass['S'] = 'on';
215        if($data['round']) $pass['r'] = 'on';
216        if(!$data['edgesep']) $pass['E'] = 'on';
217
218        $img = $http->post('http://ditaa.org/ditaa/render', $pass);
219        if(!$img) return false;
220
221        return io_saveFile($out, $img);
222    }
223
224    /**
225     * Run the ditaa Java program
226     */
227    function _run($data, $in, $out) {
228        global $conf;
229
230        if(!file_exists($in)) {
231            if($conf['debug']) {
232                dbglog($in, 'no such ditaa input file');
233            }
234            return false;
235        }
236
237        $cmd = $this->getConf('java');
238        $cmd .= ' -Djava.awt.headless=true -Dfile.encoding=UTF-8 -jar';
239        $cmd .= ' ' . escapeshellarg(dirname(__FILE__) . '/ditaa/ditaa0_9.jar'); //ditaa jar
240        $cmd .= ' --encoding UTF-8';
241        $cmd .= ' ' . escapeshellarg($in); //input
242        $cmd .= ' ' . escapeshellarg($out); //output
243        $cmd .= ' -s ' . escapeshellarg($data['scale']);
244        if(!$data['antialias']) $cmd .= ' -A';
245        if(!$data['shadow']) $cmd .= ' -S';
246        if($data['round']) $cmd .= ' -r';
247        if(!$data['edgesep']) $cmd .= ' -E';
248
249        exec($cmd, $output, $error);
250
251        if($error != 0) {
252            if($conf['debug']) {
253                dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd);
254            }
255            return false;
256        }
257
258        return true;
259    }
260
261    /**
262     * Run the ditaa Go program
263     */
264    function _runGo($data, $in, $out) {
265        global $conf;
266
267        if(!file_exists($in)) {
268            if($conf['debug']) {
269                dbglog($in, 'no such ditaa input file');
270            }
271            return false;
272        }
273
274        $cmd = $this->getLocalBinary();
275        if(!$cmd) return false;
276        $cmd .= ' ' . escapeshellarg($in); //input
277        $cmd .= ' ' . escapeshellarg($out); //output
278
279        exec($cmd, $output, $error);
280
281        if($error != 0) {
282            if($conf['debug']) {
283                dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd);
284            }
285            return false;
286        }
287
288        return true;
289    }
290
291    /**
292     * Detects the platform of the PHP host and constructs the appropriate binary name
293     *
294     * @return false|string
295     */
296    protected function getBinaryName() {
297        $ext = '';
298
299        $os = php_uname('s');
300        if(preg_match('/darwin/i', $os)) {
301            $os = 'darwin';
302        } elseif(preg_match('/win/i', $os)) {
303            $os = 'windows';
304            $ext = '.exe';
305        } elseif(preg_match('/linux/i', $os)) {
306            $os = 'linux';
307        } elseif(preg_match('/freebsd/i', $os)) {
308            $os = 'freebsd';
309        } elseif(preg_match('/openbsd/i', $os)) {
310            $os = 'openbsd';
311        } elseif(preg_match('/netbsd/i', $os)) {
312            $os = 'netbsd';
313        } elseif(preg_match('/(solaris|netbsd)/i', $os)) {
314            $os = 'freebsd';
315        } else {
316            return false;
317        }
318
319        $arch = php_uname('m');
320        if($arch == 'x86_64') {
321            $arch = 'amd64';
322        } elseif(preg_match('/arm/i', $arch)) {
323            $arch = 'amd';
324        } else {
325            $arch = '386';
326        }
327
328        return "ditaa-$os-$arch$ext";
329    }
330
331    /**
332     * Returns the local binary to use
333     *
334     * @return bool|string
335     */
336    protected function getLocalBinary() {
337        global $conf;
338
339        $bin = $this->getBinaryName();
340        if(!$bin) return false;
341
342        // check distributed files first
343        if(file_exists(__DIR__.'/ditaa/'.$bin)) {
344            return __DIR__.'/ditaa/'.$bin;
345        }
346
347        $info = $this->getInfo();
348        $cache = getCacheName($info['date'], ".$bin");
349
350        if(file_exists($cache)) return $cache;
351
352        $url = 'https://github.com/splitbrain/dokuwiki-plugin-ditaa/raw/bins/'.$bin;
353        if(io_download($url, $cache, false, '', 0)) {
354            @chmod($cache, $conf['dmode']);
355            return $cache;
356        }
357
358        return false;
359    }
360}
361
362