1<?php 2 3use dokuwiki\Extension\SyntaxPlugin; 4use dokuwiki\HTTP\DokuHTTPClient; 5use dokuwiki\Logger; 6 7/** 8 * graphviz-Plugin: Parses graphviz-blocks 9 * 10 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 11 * @author Carl-Christian Salvesen <calle@ioslo.net> 12 * @author Andreas Gohr <andi@splitbrain.org> 13 */ 14class syntax_plugin_graphviz extends SyntaxPlugin 15{ 16 /** 17 * What about paragraphs? 18 */ 19 public function getPType() 20 { 21 return 'normal'; 22 } 23 24 /** 25 * What kind of syntax are we? 26 */ 27 public function getType() 28 { 29 return 'substition'; 30 } 31 32 /** 33 * Where to sort in? 34 */ 35 public function getSort() 36 { 37 return 200; 38 } 39 40 /** 41 * Connect pattern to lexer 42 */ 43 public function connectTo($mode) 44 { 45 $this->Lexer->addSpecialPattern('<graphviz.*?>\n.*?\n</graphviz>', $mode, 'plugin_graphviz'); 46 } 47 48 /** 49 * Handle the match 50 */ 51 public function handle($match, $state, $pos, Doku_Handler $handler) 52 { 53 $info = $this->getInfo(); 54 55 // prepare default data 56 $return = [ 57 'width' => 0, 58 'height' => 0, 59 'layout' => 'dot', 60 'align' => '', 61 'version' => $info['date'] 62 ]; 63 64 // prepare input 65 $lines = explode("\n", $match); 66 $conf = array_shift($lines); 67 array_pop($lines); 68 69 // match config options 70 if (preg_match('/\b(left|center|right)\b/i', $conf, $match)) $return['align'] = $match[1]; 71 if (preg_match('/\b(\d+)x(\d+)\b/', $conf, $match)) { 72 $return['width'] = $match[1]; 73 $return['height'] = $match[2]; 74 } 75 if (preg_match('/\b(dot|neato|twopi|circo|fdp)\b/i', $conf, $match)) { 76 $return['layout'] = strtolower($match[1]); 77 } 78 if (preg_match('/\bwidth=([0-9]+)\b/i', $conf, $match)) $return['width'] = $match[1]; 79 if (preg_match('/\bheight=([0-9]+)\b/i', $conf, $match)) $return['height'] = $match[1]; 80 81 82 $input = implode("\n", $lines); 83 $return['md5'] = md5($input); // we only pass a hash around 84 85 // store input for later use 86 io_saveFile($this->getCachename($return, 'txt'), $input); 87 88 return $return; 89 } 90 91 /** 92 * Create output 93 */ 94 public function render($format, Doku_Renderer $R, $data) 95 { 96 if ($format == 'xhtml') { 97 $img = DOKU_BASE . 'lib/plugins/graphviz/img.php?' . buildURLparams($data); 98 $R->doc .= '<img src="' . $img . '" class="media' . $data['align'] . ' plugin_graphviz" alt=""'; 99 if ($data['width']) $R->doc .= ' width="' . $data['width'] . '"'; 100 if ($data['height']) $R->doc .= ' height="' . $data['height'] . '"'; 101 if ($data['align'] == 'right') $R->doc .= ' align="right"'; 102 if ($data['align'] == 'left') $R->doc .= ' align="left"'; 103 $R->doc .= '/>'; 104 return true; 105 } elseif ($format == 'odt') { 106 /** @var Doku_Renderer_odt $R */ 107 $src = $this->imgFile($data); 108 $R->_odtAddImage($src, $data['width'], $data['height'], $data['align']); 109 return true; 110 } 111 return false; 112 } 113 114 /** 115 * Cache file is based on parameters that influence the result image 116 */ 117 protected function getCachename($data, $ext) 118 { 119 unset($data['width']); 120 unset($data['height']); 121 unset($data['align']); 122 return getcachename(implode('x', array_values($data)), '.graphviz.' . $ext); 123 } 124 125 /** 126 * Return path to the rendered image on our local system 127 */ 128 public function imgFile($data) 129 { 130 $cache = $this->getCachename($data, 'svg'); 131 132 // create the file if needed 133 if (!file_exists($cache)) { 134 $in = $this->getCachename($data, 'txt'); 135 if ($this->getConf('path')) { 136 $ok = $this->renderLocal($data, $in, $cache); 137 } else { 138 $ok = $this->renderRemote($data, $in, $cache); 139 } 140 if (!$ok) return false; 141 clearstatcache(); 142 } 143 144 // something went wrong, we're missing the file 145 if (!file_exists($cache)) return false; 146 147 return $cache; 148 } 149 150 /** 151 * Render the output remotely at google 152 * 153 * @param array $data The graphviz data 154 * @param string $in The input file path 155 * @param string $out The output file path 156 */ 157 protected function renderRemote($data, $in, $out) 158 { 159 if (!file_exists($in)) { 160 Logger::debug("Graphviz: missing input file $in"); 161 return false; 162 } 163 164 $http = new DokuHTTPClient(); 165 $http->timeout = 30; 166 $http->headers['Content-Type'] = 'application/json'; 167 168 $pass = []; 169 $pass['layout'] = $data['layout']; 170 $pass['graph'] = io_readFile($in); 171 #if($data['width']) $pass['width'] = (int) $data['width']; 172 #if($data['height']) $pass['height'] = (int) $data['height']; 173 174 $img = $http->post('https://quickchart.io/graphviz', json_encode($pass)); 175 if (!$img) { 176 Logger::debug("Graphviz: remote API call failed", $http->resp_body); 177 return false; 178 } 179 180 return io_saveFile($out, $img); 181 } 182 183 /** 184 * Run the graphviz program 185 * 186 * @param array $data The graphviz data 187 * @param string $in The input file path 188 * @param string $out The output file path 189 */ 190 public function renderLocal($data, $in, $out) 191 { 192 if (!file_exists($in)) { 193 Logger::debug("Graphviz: missing input file $in"); 194 return false; 195 } 196 197 $cmd = $this->getConf('path'); 198 $cmd .= ' -Tsvg'; 199 $cmd .= ' -K' . $data['layout']; 200 $cmd .= ' -o' . escapeshellarg($out); //output 201 $cmd .= ' ' . escapeshellarg($in); //input 202 203 exec($cmd, $output, $error); 204 205 if ($error != 0) { 206 Logger::debug("Graphviz: command failed $cmd", implode("\n", $output)); 207 return false; 208 } 209 return true; 210 } 211} 212