1<?php 2/** 3 * DokuWiki Plugin json (Syntax Component) 4 * 5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 6 * @author Janez Paternoster <janez.paternoster@siol.net> 7 */ 8 9// must be run within Dokuwiki 10if (!defined('DOKU_INC')) { 11 die(); 12} 13 14 15class syntax_plugin_json_define extends DokuWiki_Syntax_Plugin 16{ 17 /** 18 * @return string Syntax mode type 19 */ 20 public function getType() { 21 return 'protected'; 22 } 23 24 25 /** 26 * @return string Paragraph type 27 */ 28 public function getPType() { 29 return 'block'; 30 } 31 32 33 /** 34 * @return int Sort order - Low numbers go before high numbers 35 */ 36 public function getSort() { 37 return 185; 38 } 39 40 41 /** 42 * Connect lookup pattern to lexer. 43 * 44 * @param string $mode Parser mode 45 */ 46 public function connectTo($mode) { 47 $this->Lexer->addSpecialPattern('<json[a-z0-9]*\b.*?>.*?</json[a-z0-9]*>', $mode, 'plugin_json_define'); 48 } 49 50 51 /** 52 * Handle matches of the json syntax 53 * 54 * @param string $match The match of the syntax 55 * @param int $state The state of the handler 56 * @param int $pos The position in the document 57 * @param Doku_Handler $handler The handler 58 * 59 * @return array Data for the renderer 60 */ 61 public function handle($match, $state, $pos, Doku_Handler $handler) { 62 $json_o = $this->loadHelper('json'); 63 $data = $json_o->handle_element($match); 64 65 if($data === NULL) { 66 return $match; 67 } 68 69 //is there a plugin 70 if(!isset($data['error']) && $data['tag'] !== 'json') { 71 $sub_plugin = $this->loadHelper($data['tag']); 72 if(!($sub_plugin && is_a($sub_plugin, 'helper_plugin_json'))) { 73 unset($sub_plugin); 74 if($this->getConf('ignore_if_no_plugin')) { 75 return $match; 76 } 77 } 78 } 79 80 //get attribute 'display' with display options, separated by commas 81 if(isset($data['keys']['display'])) { 82 if($data['keys']['display'][0] === ',') { 83 //add options to defaults 84 $data['display'] = 85 strtolower($this->getConf('json_display')). 86 strtolower($data['keys']['display']); 87 } 88 else { 89 //use only custom display options 90 $data['display'] = strtolower($data['keys']['display']); 91 } 92 } 93 else { 94 //use default display options 95 $data['display'] = strtolower($this->getConf('json_display')); 96 } 97 98 //Include data, if archive=make 99 if(!isset($data['error']) && isset($data['keys']['archive'])) { 100 if(strtolower($data['keys']['archive']) === 'make') { 101 $data['display'] .= ',orig-hidden'; 102 } 103 } 104 105 //call a sub-plugin 106 if(!isset($data['error']) && isset($sub_plugin)) { 107 $data['sub_plugin'] = true; 108 $sub_plugin->handle($data); 109 } 110 111 return $data; 112 } 113 114 115 /** 116 * Render xhtml output or metadata 117 * 118 * @param string $mode Renderer mode (supported modes: xhtml) 119 * @param Doku_Renderer $renderer The renderer 120 * @param array $data The data from the handler() function 121 * 122 * @return bool If rendering was successful. 123 */ 124 public function render($mode, Doku_Renderer $renderer, $data) { 125 126 if($mode === 'metadata') { 127 if(!isset($data['error']) && isset($data['src']['internallink'])) { 128 $renderer->internallink($data['src']['internallink']); 129 } 130 } 131 132 else if($mode === 'xhtml') { 133 $json_o = $this->loadHelper('json'); 134 $data_path = isset($data['keys']['path']) ? $data['keys']['path'] : ''; 135 136 if(is_string($data)) { 137 $renderer->cdata($data); 138 return true; 139 } 140 141 static $tab_number = 0; 142 $tab_number++; 143 144 $log = array('tag' => $data['tag'], 'id' => $data['id'] ?? '', 'path' => $data_path, 'inline' => (strlen(trim($data['json_inline_raw'])) > 0)); 145 146 //buld the json database 147 if(!isset($data['error'])) { 148 if(!isset($data['src_archive'])) { 149 //check, if src to json file is specified in query string 150 if(isset($data['keys']['src_ext'])) { 151 $src = NULL; 152 $src_ext = strtolower($data['keys']['src_ext']); 153 if(preg_match('/^json_\w+$/', $src_ext)) { 154 //scan query string for matching key 155 foreach ($_GET as $q_key => $q_val) { 156 if(strtolower($q_key) == $src_ext) { 157 $src = $json_o->parse_src($q_val); 158 break; 159 } 160 } 161 } 162 if(is_string($src)) { 163 $data['src'] = $src; 164 $data['src_extractors'] = $json_o->extractors_handle($src); 165 } 166 else if(is_array($src)) { 167 $data['src'] = $src; 168 } 169 else if(!isset($data['src'])) { 170 $log['error'] = 'query string for src_ext='.$data['keys']['src_ext'].' not defined'; 171 } 172 } 173 174 //check, if src_path to json file is specified in query string 175 if(isset($data['keys']['src_path_ext'])) { 176 $src_path = NULL; 177 $src_path_ext = strtolower($data['keys']['src_path_ext']); 178 if(preg_match('/^json_\w+$/', $src_path_ext)) { 179 //scan query string for matching key 180 foreach ($_GET as $q_key => $q_val) { 181 if(strtolower($q_key) == $src_path_ext) { 182 $src_path = $json_o->parse_tokens($q_val); 183 break; 184 } 185 } 186 } 187 if(is_array($src_path)) { 188 $data['src_path'] = $src_path; 189 } 190 else if(!is_array($data['src_path'])) { 191 $log['error'] = 'query string for src_path_ext='.$data['keys']['src_path_ext'].' not defined'; 192 } 193 } 194 195 //disable browser cache, if external files are used for data 196 if(isset($data['src']) && is_array($data['src'])) { 197 $renderer->nocache(); 198 } 199 } 200 201 //load all json data and put it into the json database 202 $json_o->add_json(helper_plugin_json::$json, $data, $this->getConf('src_recursive'), $log); 203 } 204 else { 205 $log['error'] = $data['error']; 206 } 207 208 209 //prapare data for html output (jQuery UI tabs) 210 $class = array('json-tabs'); 211 if(isset($data['make_archive'])) { 212 $class[] = 'json-make-archive'; 213 } 214 $data_attr = array( 215 'json-id' => $data['id'] ?? '', 216 'json-hash' => md5($data['json_inline_raw']), 217 'active' => 'false'); //all tabs colapsed or specific tab active 218 $tabs = array(); 219 $body = array(); 220 $display = $data['display']; 221 $all = strpos($display, 'all') !== false; 222 $tab_no = 0; 223 224 //json original data (before they are combined with inline data) 225 if($all || strpos($display, 'original') !== false) { 226 if(strpos($display, 'original*') !== false) { $data_attr['active'] = $tab_no; } 227 $tab_no++; 228 $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-orig">'.$this->getLang('json_original').'</a></li>'; 229 $body[] = '<div id="json-tab-'.$tab_number.'-orig"><pre class="json-data-original lang-json">' 230 .htmlspecialchars(json_encode($data['json_original'], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)).'</pre></div>'; 231 } 232 else if(strpos($display, 'orig-hidden') !== false) { 233 $body[] = '<div hidden=""><pre class="json-data-original">' 234 .htmlspecialchars(json_encode($data['json_original'])).'</pre></div>'; 235 } 236 237 //json inline data 238 if($all || strpos($display, 'inline') !== false) { 239 if(strpos($display, 'inline*') !== false) { $data_attr['active'] = $tab_no; } 240 $tab_no++; 241 $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-inline">'.$this->getLang('json_inline').'</a></li>'; 242 $body[] = '<div id="json-tab-'.$tab_number.'-inline"><textarea wrap="off" class="json-data-inline json-textarea">' 243 .htmlspecialchars($data['json_inline_raw']).'</textarea></div>'; 244 } 245 else if(strpos($display, 'inl-hidden') !== false) { 246 $body[] = '<div hidden=""><textarea class="json-data-inline">' 247 .htmlspecialchars($data['json_inline_raw']).'</textarea></div>'; 248 } 249 250 //json combined data 251 if($all || strpos($display, 'combined') !== false) { 252 if(strpos($display, 'combined*') !== false || ($all && $data_attr['active'] === 'false')) { $data_attr['active'] = $tab_no; } 253 $tab_no++; 254 $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-comb">'.$this->getLang('json_combined').'</a></li>'; 255 $body[] = '<div id="json-tab-'.$tab_number.'-comb"><pre class="json-data-combined lang-json">' 256 .htmlspecialchars(json_encode($data['json_combined'], JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)).'</pre></div>'; 257 } 258 else if(strpos($display, 'comb-hidden') !== false) { 259 $body[] = '<div hidden=""><pre class="json-data-combined">' 260 .htmlspecialchars(json_encode($data['json_combined'])).'</pre></div>'; 261 } 262 263 //call a sub-plugin 264 if(isset($data['sub_plugin'])) { 265 $sub_plugin = $this->loadHelper($data['tag']); 266 $sub_plugin->render($renderer, $data, $class, $data_attr, $tabs, $body, $log, $tab_no, $tab_number); 267 } 268 269 //display 'error' log when there are errors or display 'log', when there are external source files. 270 if(($all || (strpos($display, 'error') !== false) || (strpos($display, 'log') !== false)) && $this->find_key('error', $log)) { 271 if(strpos($display, 'error*') !== false || strpos($display, 'log*') !== false) { $data_attr['active'] = $tab_no; } 272 $tab_no++; 273 $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-err" class="json-error">'.$this->getLang('error').'</a></li>'; 274 $body[] = '<div id="json-tab-' .$tab_number.'-err" class="json-log">'.$this->render_log($renderer, array($log)).'</div>'; 275 } 276 else if($all || (strpos($display, 'log') !== false)) { 277 if(strpos($display, 'log*') !== false) { $data_attr['active'] = $tab_no; } 278 $tab_no++; 279 $tabs[] = '<li><a href="#json-tab-'.$tab_number.'-log">'.$this->getLang('log').'</a></li>'; 280 $body[] = '<div id="json-tab-'.$tab_number.'-log" class="json-log">'.$this->render_log($renderer, array($log)).'</div>'; 281 } 282 283 //no tabs, completelly hide the element 284 if($tab_no === 0) { 285 $class[] = 'json-hidden'; 286 } 287 //if single tab is there and is set to default, then hide tabs menu 288 else if($tab_no === 1 && $data_attr['active'] !== 'false') { 289 $class[] = 'json-hide-tabs'; 290 } 291 292 //write html 293 if(count($body) > 0) { 294 $renderer->doc .= '<div class="'.implode(' ', $class).'" '.$this->implode_data_attr($data_attr).'>' 295 ."\n<ul>\n" 296 ." <li><a>".$data_path."</a> <button class='json-save-button'>".$this->getLang('save')."</button></li>\n " 297 .implode("\n ", $tabs) 298 ."\n</ul>\n" 299 .implode("\n", $body) 300 ."\n</div>"; 301 } 302 } 303 return true; 304 } 305 306 307 /** 308 * Verify, if key exists in multidimensional array 309 */ 310 private function find_key($keySearch, $array) { 311 foreach($array as $key => $item) { 312 if($key === $keySearch) { 313 return true; 314 } elseif (is_array($item) && $this->find_key($keySearch, $item)) { 315 return true; 316 } 317 } 318 return false; 319 } 320 321 322 /** 323 * return string with html data-... attributes 324 */ 325 private function implode_data_attr($arr) { 326 $s = []; 327 foreach($arr as $key => $val) { 328 $s[] = 'data-'.$key.'="'.$val.'"'; 329 } 330 return implode(' ', $s); 331 } 332 333 334 /** 335 * Render json log 336 * 337 * @param Doku_Renderer $r The renderer 338 * @param array $log Log data elements from <json> element 339 * @param integer $level list level 340 * 341 * @return string html list with info about JSON data source 342 */ 343 private function render_log(Doku_Renderer $renderer, $log_elements, $level=1) { 344 $doc = '<ul>'.DOKU_LF; 345 346 foreach($log_elements as $el) { 347 348 //listitem with info about <json> element 349 $doc .= '<li class="level'.$level.'"><div class="li">'; 350 $doc .= 'element: '.$el['tag']; 351 if ($el['id']) $doc .= ', id: '.$el['id']; 352 $doc .= ', path: '.htmlspecialchars($el['path']); 353 if(isset($el['error'])) { 354 $doc .= ', <span class="json-error">ERROR</span>: '.htmlspecialchars($el['error']); 355 } 356 if(!empty($el['src_archive'])) { 357 $doc .= ', archived src data'; 358 } 359 if($el['inline']) { 360 $doc .= ', inline data'; 361 } 362 if(isset($el['src'])) { 363 $doc .= ', external data'; 364 if(isset($el['src_path'])) { 365 $doc .= ' (from path: '.htmlspecialchars($el['src_path']).')'; 366 } 367 } 368 $doc .= '</div></li>'.DOKU_LF; 369 370 //list of files with external json data 371 if(isset($el['src'])) { 372 $doc .= '<ul>'.DOKU_LF; 373 foreach($el['src'] as $file) { 374 $doc .= '<li class="level'.($level+1).'"><div class="li">'; 375 if($file['filename'] === '***JSON code***') { 376 $doc .= 'JSON code from \'src\' attribute'; 377 } 378 else if(isset($file['extenal_link'])) { 379 $doc .= 'external file: '.$renderer->externallink($file['filename'], $file['filename'], true); 380 } 381 else { 382 $doc .= 'internal file: '.$renderer->internallink($file['filename'], $file['filename'], null, true); 383 } 384 if(isset($file['error'])) { 385 $doc .= ', <span class="json-error">ERROR</span>: '.htmlspecialchars($file['error']); 386 } 387 else if(!isset($file['elements']) && $file['filename'] !== '***JSON code***') { 388 $doc .= ', JSON file'; 389 } 390 $doc .= '</div></li>'.DOKU_LF; 391 if(isset($file['elements'])) { 392 $doc .= $this->render_log($renderer, $file['elements'], $level+2); 393 } 394 } 395 $doc .= '</ul>'.DOKU_LF; 396 } 397 } 398 399 $doc .= '</ul>'.DOKU_LF; 400 401 return $doc; 402 } 403 404} 405