xref: /plugin/ditaa/syntax.php (revision 35cad79268be8e2b877be727d6f84846ee2d123e)
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
17/**
18 * Class syntax_plugin_ditaa
19 */
20class syntax_plugin_ditaa extends DokuWiki_Syntax_Plugin {
21
22    /**
23     * What about paragraphs?
24     */
25    public function getPType() {
26        return 'normal';
27    }
28
29    /**
30     * What kind of syntax are we?
31     */
32    public function getType() {
33        return 'substition';
34    }
35
36    /**
37     * Where to sort in?
38     */
39    public function getSort() {
40        return 200;
41    }
42
43    /**
44     * Connect pattern to lexer
45     *
46     * @param string $mode
47     */
48    public function connectTo($mode) {
49        $this->Lexer->addSpecialPattern('<ditaa.*?>\n.*?\n</ditaa>', $mode, 'plugin_ditaa');
50    }
51
52    /**
53     * Stores all infor about the diagram in two files. One is the actual ditaa data, the other
54     * contains the options.
55     *
56     * @param   string $match The text matched by the patterns
57     * @param   int $state The lexer state for the match
58     * @param   int $pos The character position of the matched text
59     * @param   Doku_Handler $handler The Doku_Handler object
60     * @return  bool|array Return an array with all data you want to use in render, false don't add an instruction
61     */
62    public function handle($match, $state, $pos, Doku_Handler $handler) {
63        $info = $this->getInfo();
64
65        // prepare default data
66        $return = array(
67            'width' => 0,
68            'height' => 0,
69            'antialias' => true,
70            'edgesep' => true,
71            'round' => false,
72            'shadow' => true,
73            'scale' => 1,
74            'align' => '',
75            'version' => $info['date'], //force rebuild of images on update
76        );
77
78        // prepare input
79        $lines = explode("\n", $match);
80        $conf = array_shift($lines);
81        array_pop($lines);
82
83        // match config options
84        if(preg_match('/\b(left|center|right)\b/i', $conf, $match)) $return['align'] = $match[1];
85        if(preg_match('/\b(\d+)x(\d+)\b/', $conf, $match)) {
86            $return['width'] = $match[1];
87            $return['height'] = $match[2];
88        }
89        if(preg_match('/\b(\d+(\.\d+)?)X\b/', $conf, $match)) $return['scale'] = $match[1];
90        if(preg_match('/\bwidth=([0-9]+)\b/i', $conf, $match)) $return['width'] = $match[1];
91        if(preg_match('/\bheight=([0-9]+)\b/i', $conf, $match)) $return['height'] = $match[1];
92        // match boolean toggles
93        if(preg_match_all('/\b(no)?(antialias|edgesep|round|shadow)\b/i', $conf, $matches, PREG_SET_ORDER)) {
94            foreach($matches as $match) {
95                $return[$match[2]] = !$match[1];
96            }
97        }
98
99        $input = join("\n", $lines);
100        $return['md5'] = md5($input); // we only pass a hash around
101
102        // store input for later use in _imagefile()
103        io_saveFile(getCacheName($return['md5'], '.ditaa.txt'), $input);
104        io_saveFile(getCacheName($return['md5'], '.ditaa.cfg'), serialize($return));
105
106        return $return;
107    }
108
109    /**
110     * Output the image
111     *
112     * @param string $format output format being rendered
113     * @param Doku_Renderer $R the current renderer object
114     * @param array $data data created by handler()
115     * @return  boolean                 rendered correctly?
116     */
117    public function render($format, Doku_Renderer $R, $data) {
118        global $ID;
119        if($format == 'xhtml') {
120            // Only use the md5 key
121            $img = ml($ID, array('ditaa' => $data['md5']));
122            $R->doc .= '<img src="' . $img . '" class="media' . $data['align'] . '" alt=""';
123            if($data['width']) $R->doc .= ' width="' . $data['width'] . '"';
124            if($data['height']) $R->doc .= ' height="' . $data['height'] . '"';
125            if($data['align'] == 'right') $R->doc .= ' align="right"';
126            if($data['align'] == 'left') $R->doc .= ' align="left"';
127            $R->doc .= '/>';
128            return true;
129        } else if($format == 'odt') {
130            $src = $this->_imgfile($data['md5']);
131            /** @var  renderer_plugin_odt $R */
132            $R->_odtAddImage($src, $data['width'], $data['height'], $data['align']);
133            return true;
134        }
135        return false;
136    }
137
138    /**
139     * Return path to the rendered image on our local system
140     *
141     * @param string $md5 MD5 of the input data, used to identify the cache files
142     * @return false|string path to file or fals on error
143     */
144    public function _imgfile($md5) {
145        $file_cfg = getCacheName($md5, '.ditaa.cfg'); // configs
146        $file_txt = getCacheName($md5, '.ditaa.txt'); // input
147        $file_png = getCacheName($md5, '.ditaa.png'); // ouput
148
149        if(!file_exists($file_cfg) || !file_exists($file_txt)) {
150            return false;
151        }
152        $data = unserialize(io_readFile($file_cfg, false));
153
154        // file does not exist or is outdated
155        if(@filemtime($file_png) < filemtime($file_cfg)) {
156
157            if($this->getConf('java')) {
158                $ok = $this->_runJava($data, $file_txt, $file_png);
159            } else {
160                $ok = $this->_runGo($data, $file_txt, $file_png);
161                #$ok = $this->_remote($data, $in, $cache);
162            }
163            if(!$ok) return false;
164
165            clearstatcache($file_png);
166        }
167
168        // resized version
169        if($data['width']) {
170            $file_png = media_resize_image($file_png, 'png', $data['width'], $data['height']);
171        }
172
173        // something went wrong, we're missing the file
174        if(!file_exists($file_png)) return false;
175
176        return $file_png;
177    }
178
179    /**
180     * Render the output remotely at ditaa.org
181     *
182     * @deprecated ditaa.org is no longer available, so this defunct
183     * @param array $data The config settings
184     * @param string $in Path to the ditaa input file (txt)
185     * @param string $out Path to the output file (PNG)
186     * @return bool true if the image was created, false otherwise
187     */
188    protected function _remote($data, $in, $out) {
189        global $conf;
190
191        if(!file_exists($in)) {
192            if($conf['debug']) {
193                dbglog($in, 'no such ditaa input file');
194            }
195            return false;
196        }
197
198        $http = new DokuHTTPClient();
199        $http->timeout = 30;
200
201        $pass = array();
202        $pass['scale'] = $data['scale'];
203        $pass['timeout'] = 25;
204        $pass['grid'] = io_readFile($in);
205        if(!$data['antialias']) $pass['A'] = 'on';
206        if(!$data['shadow']) $pass['S'] = 'on';
207        if($data['round']) $pass['r'] = 'on';
208        if(!$data['edgesep']) $pass['E'] = 'on';
209
210        $img = $http->post('http://ditaa.org/ditaa/render', $pass);
211        if(!$img) return false;
212
213        return io_saveFile($out, $img);
214    }
215
216    /**
217     * Run the ditaa Java program
218     *
219     * @param array $data The config settings
220     * @param string $in Path to the ditaa input file (txt)
221     * @param string $out Path to the output file (PNG)
222     * @return bool true if the image was created, false otherwise
223     */
224    protected function _runJava($data, $in, $out) {
225        global $conf;
226
227        if(!file_exists($in)) {
228            if($conf['debug']) {
229                dbglog($in, 'no such ditaa input file');
230            }
231            return false;
232        }
233
234        $cmd = $this->getConf('java');
235        $cmd .= ' -Djava.awt.headless=true -Dfile.encoding=UTF-8 -jar';
236        $cmd .= ' ' . escapeshellarg(dirname(__FILE__) . '/ditaa/ditaa0_9.jar'); //ditaa jar
237        $cmd .= ' --encoding UTF-8';
238        $cmd .= ' ' . escapeshellarg($in); //input
239        $cmd .= ' ' . escapeshellarg($out); //output
240        $cmd .= ' -s ' . escapeshellarg($data['scale']);
241        if(!$data['antialias']) $cmd .= ' -A';
242        if(!$data['shadow']) $cmd .= ' -S';
243        if($data['round']) $cmd .= ' -r';
244        if(!$data['edgesep']) $cmd .= ' -E';
245
246        exec($cmd, $output, $error);
247
248        if($error != 0) {
249            if($conf['debug']) {
250                dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd);
251            }
252            return false;
253        }
254
255        return true;
256    }
257
258    /**
259     * Run the ditaa Go program
260     *
261     * @param array $data The config settings - currently not used because the Go relase supports no options
262     * @param string $in Path to the ditaa input file (txt)
263     * @param string $out Path to the output file (PNG)
264     * @return bool true if the image was created, false otherwise
265     */
266    protected function _runGo($data, $in, $out) {
267        global $conf;
268
269        if(!file_exists($in)) {
270            if($conf['debug']) {
271                dbglog($in, 'no such ditaa input file');
272            }
273            return false;
274        }
275
276        $cmd = $this->getLocalBinary();
277        if(!$cmd) return false;
278        $cmd .= ' ' . escapeshellarg($in); //input
279        $cmd .= ' ' . escapeshellarg($out); //output
280
281        exec($cmd, $output, $error);
282
283        if($error != 0) {
284            if($conf['debug']) {
285                dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd);
286            }
287            return false;
288        }
289
290        return true;
291    }
292
293    /**
294     * Detects the platform of the PHP host and constructs the appropriate binary name
295     *
296     * @return false|string
297     */
298    protected function getBinaryName() {
299        $ext = '';
300
301        $os = php_uname('s');
302        if(preg_match('/darwin/i', $os)) {
303            $os = 'darwin';
304        } elseif(preg_match('/win/i', $os)) {
305            $os = 'windows';
306            $ext = '.exe';
307        } elseif(preg_match('/linux/i', $os)) {
308            $os = 'linux';
309        } elseif(preg_match('/freebsd/i', $os)) {
310            $os = 'freebsd';
311        } elseif(preg_match('/openbsd/i', $os)) {
312            $os = 'openbsd';
313        } elseif(preg_match('/netbsd/i', $os)) {
314            $os = 'netbsd';
315        } elseif(preg_match('/(solaris|netbsd)/i', $os)) {
316            $os = 'freebsd';
317        } else {
318            return false;
319        }
320
321        $arch = php_uname('m');
322        if($arch == 'x86_64') {
323            $arch = 'amd64';
324        } elseif(preg_match('/arm/i', $arch)) {
325            $arch = 'amd';
326        } else {
327            $arch = '386';
328        }
329
330        return "ditaa-$os-$arch$ext";
331    }
332
333    /**
334     * Returns the local binary to use
335     *
336     * Downloads it if necessary
337     *
338     * @return bool|string
339     */
340    protected function getLocalBinary() {
341        global $conf;
342
343        $bin = $this->getBinaryName();
344        if(!$bin) return false;
345
346        // check distributed files first
347        if(file_exists(__DIR__ . '/ditaa/' . $bin)) {
348            return __DIR__ . '/ditaa/' . $bin;
349        }
350
351        $info = $this->getInfo();
352        $cache = getCacheName($info['date'], ".$bin");
353
354        if(file_exists($cache)) return $cache;
355
356        $url = 'https://github.com/splitbrain/dokuwiki-plugin-ditaa/raw/bins/' . $bin;
357        if(io_download($url, $cache, false, '', 0)) {
358            @chmod($cache, $conf['dmode']);
359            return $cache;
360        }
361
362        return false;
363    }
364}
365
366