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'], 76 'now' => time() 77 ); 78 79 // prepare input 80 $lines = explode("\n", $match); 81 $conf = array_shift($lines); 82 array_pop($lines); 83 84 // match config options 85 if(preg_match('/\b(left|center|right)\b/i', $conf, $match)) $return['align'] = $match[1]; 86 if(preg_match('/\b(\d+)x(\d+)\b/', $conf, $match)) { 87 $return['width'] = $match[1]; 88 $return['height'] = $match[2]; 89 } 90 if(preg_match('/\b(\d+(\.\d+)?)X\b/', $conf, $match)) $return['scale'] = $match[1]; 91 if(preg_match('/\bwidth=([0-9]+)\b/i', $conf, $match)) $return['width'] = $match[1]; 92 if(preg_match('/\bheight=([0-9]+)\b/i', $conf, $match)) $return['height'] = $match[1]; 93 // match boolean toggles 94 if(preg_match_all('/\b(no)?(antialias|edgesep|round|shadow)\b/i', $conf, $matches, PREG_SET_ORDER)) { 95 foreach($matches as $match) { 96 $return[$match[2]] = !$match[1]; 97 } 98 } 99 100 $input = join("\n", $lines); 101 $return['md5'] = md5($input.$this->_prepareData($return)); // we only pass a hash around 102 103 // store input for later use in _imagefile() 104 io_saveFile(getCacheName($return['md5'], '.ditaa.txt'), $input); 105 io_saveFile(getCacheName($return['md5'], '.ditaa.cfg'), serialize($return)); 106 107 return $return; 108 } 109 110 /** 111 * Output the image 112 * 113 * @param string $format output format being rendered 114 * @param Doku_Renderer $R the current renderer object 115 * @param array $data data created by handler() 116 * @return boolean rendered correctly? 117 */ 118 public function render($format, Doku_Renderer $R, $data) { 119 global $ID; 120 if($format == 'xhtml') { 121 // Only use the md5 key 122 $img = ml($ID, array('ditaa' => $data['md5'], 't' => $data['now'])); 123 $R->doc .= '<img src="' . $img . '" class="media' . $data['align'] . '" alt=""'; 124 if($data['width']) $R->doc .= ' width="' . $data['width'] . '"'; 125 if($data['height']) $R->doc .= ' height="' . $data['height'] . '"'; 126 if($data['align'] == 'right') $R->doc .= ' align="right"'; 127 if($data['align'] == 'left') $R->doc .= ' align="left"'; 128 $R->doc .= '/>'; 129 return true; 130 } else if($format == 'odt') { 131 $src = $this->_imgfile($data['md5']); 132 /** @var renderer_plugin_odt $R */ 133 $R->_odtAddImage($src, $data['width'], $data['height'], $data['align']); 134 return true; 135 } 136 return false; 137 } 138 139 /** 140 * Prepares the Data that is used for the cache name 141 * Width, height and scale are left out. 142 * Ensures sanity. 143 */ 144 protected function _prepareData($input) { 145 $output = array(); 146 foreach($input as $key => $value) { 147 switch($key) { 148 case 'scale': 149 case 'antialias': 150 case 'edgesep': 151 case 'round': 152 case 'shadow': 153 case 'version': 154 $output[$key] = $value; 155 }; 156 } 157 ksort($output); 158 return $output; 159 } 160 161 /** 162 * Return path to the rendered image on our local system 163 * 164 * @param string $md5 MD5 of the input data, used to identify the cache files 165 * @return false|string path to file or fals on error 166 */ 167 public function _imgfile($md5) { 168 $file_cfg = getCacheName($md5, '.ditaa.cfg'); // configs 169 $file_txt = getCacheName($md5, '.ditaa.txt'); // input 170 $file_png = getCacheName($md5, '.ditaa.png'); // ouput 171 172 if(!file_exists($file_cfg) || !file_exists($file_txt)) { 173 return false; 174 } 175 $data = unserialize(io_readFile($file_cfg, false)); 176 177 // file does not exist or is outdated 178 if(@filemtime($file_png) < filemtime($file_cfg)) { 179 180 if($this->getConf('java')) { 181 $ok = $this->_runJava($data, $file_txt, $file_png); 182 } else { 183 $ok = $this->_runGo($data, $file_txt, $file_png); 184 #$ok = $this->_remote($data, $in, $cache); 185 } 186 if(!$ok) return false; 187 188 clearstatcache($file_png); 189 } 190 191 // resized version 192 if($data['width']) { 193 $file_png = media_resize_image($file_png, 'png', $data['width'], $data['height']); 194 } 195 196 // something went wrong, we're missing the file 197 if(!file_exists($file_png)) return false; 198 199 return $file_png; 200 } 201 202 /** 203 * Render the output remotely at ditaa.org 204 * 205 * @deprecated ditaa.org is no longer available, so this defunct 206 * @param array $data The config settings 207 * @param string $in Path to the ditaa input file (txt) 208 * @param string $out Path to the output file (PNG) 209 * @return bool true if the image was created, false otherwise 210 */ 211 protected function _remote($data, $in, $out) { 212 global $conf; 213 214 if(!file_exists($in)) { 215 if($conf['debug']) { 216 dbglog($in, 'no such ditaa input file'); 217 } 218 return false; 219 } 220 221 $http = new DokuHTTPClient(); 222 $http->timeout = 30; 223 224 $pass = array(); 225 $pass['scale'] = $data['scale']; 226 $pass['timeout'] = 25; 227 $pass['grid'] = io_readFile($in); 228 if(!$data['antialias']) $pass['A'] = 'on'; 229 if(!$data['shadow']) $pass['S'] = 'on'; 230 if($data['round']) $pass['r'] = 'on'; 231 if(!$data['edgesep']) $pass['E'] = 'on'; 232 233 $img = $http->post('http://ditaa.org/ditaa/render', $pass); 234 if(!$img) return false; 235 236 return io_saveFile($out, $img); 237 } 238 239 /** 240 * Run the ditaa Java program 241 * 242 * @param array $data The config settings 243 * @param string $in Path to the ditaa input file (txt) 244 * @param string $out Path to the output file (PNG) 245 * @return bool true if the image was created, false otherwise 246 */ 247 protected function _runJava($data, $in, $out) { 248 global $conf; 249 250 if(!file_exists($in)) { 251 if($conf['debug']) { 252 dbglog($in, 'no such ditaa input file'); 253 } 254 return false; 255 } 256 257 $cmd = $this->getConf('java'); 258 $cmd .= ' -Djava.awt.headless=true -Dfile.encoding=UTF-8 -jar'; 259 $cmd .= ' ' . escapeshellarg(__DIR__ . '/ditaa/ditaa.jar'); 260 $cmd .= ' --encoding UTF-8'; 261 $cmd .= ' ' . escapeshellarg($in); //input 262 $cmd .= ' ' . escapeshellarg($out); //output 263 $cmd .= ' -s ' . escapeshellarg($data['scale']); 264 if(!$data['antialias']) $cmd .= ' -A'; 265 if(!$data['shadow']) $cmd .= ' -S'; 266 if($data['round']) $cmd .= ' -r'; 267 if(!$data['edgesep']) $cmd .= ' -E'; 268 269 exec($cmd, $output, $error); 270 271 if($error != 0) { 272 if($conf['debug']) { 273 dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd); 274 } 275 return false; 276 } 277 278 return true; 279 } 280 281 /** 282 * Run the ditaa Go program 283 * 284 * @param array $data The config settings - currently not used because the Go relase supports no options 285 * @param string $in Path to the ditaa input file (txt) 286 * @param string $out Path to the output file (PNG) 287 * @return bool true if the image was created, false otherwise 288 */ 289 protected function _runGo($data, $in, $out) { 290 global $conf; 291 292 if(!file_exists($in)) { 293 if($conf['debug']) { 294 dbglog($in, 'no such ditaa input file'); 295 } 296 return false; 297 } 298 299 $cmd = $this->getLocalBinary(); 300 if(!$cmd) return false; 301 $cmd .= ' ' . escapeshellarg($in); //input 302 $cmd .= ' ' . escapeshellarg($out); //output 303 304 exec($cmd, $output, $error); 305 306 if($error != 0) { 307 if($conf['debug']) { 308 dbglog(join("\n", $output), 'ditaa command failed: ' . $cmd); 309 } 310 return false; 311 } 312 313 return true; 314 } 315 316 /** 317 * Detects the platform of the PHP host and constructs the appropriate binary name 318 * 319 * @return false|string 320 */ 321 protected function getBinaryName() { 322 $ext = ''; 323 324 $os = php_uname('s'); 325 if(preg_match('/darwin/i', $os)) { 326 $os = 'darwin'; 327 } elseif(preg_match('/win/i', $os)) { 328 $os = 'windows'; 329 $ext = '.exe'; 330 } elseif(preg_match('/linux/i', $os)) { 331 $os = 'linux'; 332 } elseif(preg_match('/freebsd/i', $os)) { 333 $os = 'freebsd'; 334 } elseif(preg_match('/openbsd/i', $os)) { 335 $os = 'openbsd'; 336 } elseif(preg_match('/netbsd/i', $os)) { 337 $os = 'netbsd'; 338 } elseif(preg_match('/(solaris|netbsd)/i', $os)) { 339 $os = 'freebsd'; 340 } else { 341 return false; 342 } 343 344 $arch = php_uname('m'); 345 if($arch == 'x86_64') { 346 $arch = 'amd64'; 347 } elseif(preg_match('/arm/i', $arch)) { 348 $arch = 'amd'; 349 } else { 350 $arch = '386'; 351 } 352 353 return "ditaa-$os-$arch$ext"; 354 } 355 356 /** 357 * Returns the local binary to use 358 * 359 * Downloads it if necessary 360 * 361 * @return bool|string 362 */ 363 protected function getLocalBinary() { 364 global $conf; 365 366 $bin = $this->getBinaryName(); 367 if(!$bin) return false; 368 369 // check distributed files first 370 if(file_exists(__DIR__ . '/ditaa/' . $bin)) { 371 return __DIR__ . '/ditaa/' . $bin; 372 } 373 374 $info = $this->getInfo(); 375 $cache = getCacheName($info['date'], ".$bin"); 376 377 if(file_exists($cache)) return $cache; 378 379 $url = 'https://github.com/akavel/ditaa/releases/download/g1.0.0/' . $bin; 380 if(io_download($url, $cache, false, '', 0)) { 381 @chmod($cache, $conf['dmode']); 382 return $cache; 383 } 384 385 return false; 386 } 387} 388 389