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 14class syntax_plugin_json_extract extends DokuWiki_Syntax_Plugin 15{ 16 /** 17 * @return string Syntax mode type 18 */ 19 public function getType() { 20 return 'substition'; 21 } 22 23 /** 24 * @return string Paragraph type 25 */ 26 public function getPType() { 27 return 'normal'; 28 } 29 30 /** 31 * @return int Sort order - Low numbers go before high numbers 32 */ 33 public function getSort() { 34 return 150; 35 } 36 37 /** 38 * Connect lookup pattern to lexer. 39 * 40 * @param string $mode Parser mode 41 */ 42 public function connectTo($mode) { 43 // %$link.to.0.var{"header 1[ß]":link.to.varx; "header2" : link2}% 44 $this->Lexer->addSpecialPattern('\%\$.*?\%', $mode, 'plugin_json_extract'); 45 } 46 47 48 /** 49 * Handle matches of the json syntax 50 * 51 * @param string $match The match of the syntax 52 * @param int $state The state of the handler 53 * @param int $pos The position in the document 54 * @param Doku_Handler $handler The handler 55 * 56 * @return array Data for the renderer - tokens(always), (one or none from) code or list or table. 57 * [[tokens] => [tok1, tok2, ...], 58 * [code] => boolean, 59 * [list] => [ 60 * name1 => [tok3, tok4, ...], 61 * name2 => [tok5, tok6, ...], 62 * ... 63 * ] 64 * [table] => [ 65 * name1 => [tok3, tok4, ...], 66 * name2 => [tok5, tok6, ...], 67 * ... 68 * ]] 69 */ 70 public function handle($match, $state, $pos, Doku_Handler $handler) { 71 $json_o = $this->loadHelper('json'); 72 73 //Return value 74 $data = array('match' => $match); 75 76 //Replace #@macro_name@# patterns with strings defined by textinsert Plugin. 77 $match = $json_o->preprocess($match); 78 79 /* match %$path [(row_filter)] {header} #format# (filter)% 80 * 81 * path - path.to.variable 82 * [] - print table, optionally use (row_filter) 83 * {header} - header description for table or links 84 * #format# - format specifier for variable 85 * (filter) - render variable only, if filter is evaluated to true 86 * 87 * for help on PCRE see: https://regexr.com/ */ 88 preg_match('/^%\$([^[\]{}#\(\)]*)(\[(?:\(.*?\))?\s*\])?\s*(\{.*?\})?\s*(#.*?#)?\s*(\(.*?\))?\s*%$/', $match, $mt); 89 list(, $path, $table, $header, $format, $filter) = array_pad($mt, 6, ''); 90 91 // remove # from format 92 if($format) { 93 $format = substr($format, 1, -1); 94 } 95 96 $data['tokens'] = $json_o->parse_tokens($path); 97 98 if($filter) { 99 $data['filter'] = $json_o->parse_filter(substr($filter, 1, -1)); 100 } 101 102 103 //table 104 if($table) { 105 $data['type'] = 'table_without_header'; 106 $table_filter = trim(substr($table, 1, -1)); //remove [] 107 if(strlen($table_filter) > 0) { 108 $table_filter = substr(trim($table_filter), 1, -1); //remove () 109 $data['table_filter'] = $json_o->parse_filter($table_filter); 110 } 111 if($header) { 112 $table_header = $json_o->parse_links(substr($header, 1, -1)); 113 if($table_header !== false) { 114 $data['type'] = 'table_with_header'; 115 $data['table_header'] = $table_header; 116 $data['format'] = array(); 117 if($format) { 118 $format_pairs = $json_o->parse_key_val($format, ':', ','); 119 if(is_array($format_pairs)) { 120 foreach ($format_pairs as &$val) { 121 $val = $this->handle_format($val); 122 } 123 $data['format'] = $format_pairs; 124 } 125 } 126 } 127 } 128 } 129 130 //list 131 else if($header) { 132 $list_header = $json_o->parse_links(substr($header, 1, -1)); 133 if($list_header !== false) { 134 $data['type'] = 'list'; 135 $data['list_header'] = $list_header; 136 $data['format'] = array(); 137 if($format) { 138 $format_pairs = $json_o->parse_key_val($format, ':', ','); 139 if(is_array($format_pairs)) { 140 foreach ($format_pairs as &$val) { 141 $val = $this->handle_format($val); 142 } 143 $data['format'] = $format_pairs; 144 } 145 } 146 } 147 } 148 149 //variable 150 else { 151 $data['type'] = 'variable'; 152 $data['format'] = $format ? $this->handle_format($format) : ''; 153 } 154 155 return $data; 156 } 157 158 159 /** 160 * Render xhtml output or metadata 161 * 162 * @param string $mode Renderer mode (supported modes: xhtml) 163 * @param Doku_Renderer $renderer The renderer 164 * @param array $data The data from the handler() function 165 * 166 * @return bool If rendering was successful. 167 */ 168 public function render($mode, Doku_Renderer $renderer, $data) { 169 if ($mode === 'xhtml') { 170 $json_o = $this->loadHelper('json'); 171 172 if(isset($data['tokens'])) { 173 if(!isset($data['filter']) || $json_o->filter($json_o->get(), $data['filter'])) { 174 $var = $json_o->get($data['tokens']); 175 } 176 else { 177 $data['type'] = 'filtered'; 178 } 179 } 180 181 switch($data['type']) { 182 case 'filtered': 183 break; 184 185 186 case 'variable': 187 $this->render_var($renderer, $var, $data['format'], 1); 188 break; 189 190 191 case 'list': 192 $tooltips = array(); 193 //get data list 194 if(is_array($data['list_header'])) { 195 $list = array(); 196 foreach($data['list_header'] as $key => $tokens) { 197 $v = $var; 198 foreach($tokens as $tok) { 199 if(is_array($v) && isset($v[$tok])) { 200 $v = $v[$tok]; 201 } 202 else { 203 $v = null; 204 break; 205 } 206 } 207 //If $name begins with '_tooltip_', it will display only as tooltip 208 if(strpos($key, '_tooltip_') === 0) { 209 $tooltips[substr($key, 9)] = $v; 210 } 211 else { 212 $list[$key] = $v; 213 } 214 } 215 } 216 else { 217 if(is_array($var)) { 218 $list = array(); 219 foreach($var as $key => $v) { 220 $list[$key] = $v; 221 } 222 } 223 else { 224 $list = array($this->getConf('null_str') => $var); 225 } 226 } 227 228 //render data list 229 $renderer->table_open(2); 230 $renderer->tabletbody_open(); 231 foreach($list as $name => $value) { 232 if(isset($tooltips[$name])) { 233 $renderer->doc .= DOKU_TAB.'<tr title="'.htmlspecialchars($tooltips[$name]).'">'.DOKU_LF.DOKU_TAB.DOKU_TAB; 234 } 235 else { 236 $renderer->tablerow_open(); 237 } 238 $renderer->tableheader_open(); 239 $renderer->cdata($name); 240 $renderer->tableheader_close(); 241 $renderer->tablecell_open(); 242 $this->render_var($renderer, $value, isset($data['format'][$name]) ? $data['format'][$name] : '', 1); 243 $renderer->tablecell_close(); 244 $renderer->tablerow_close(); 245 } 246 $renderer->tabletbody_close(); 247 $renderer->table_close(); 248 break; 249 250 251 case 'table_without_header': 252 if(!is_array($var)) { 253 $var = array($var); 254 } 255 //render table 256 $renderer->table_open(); 257 foreach($var as $set) { 258 //apply filter 259 if(isset($data['table_filter']) && !$json_o->filter($set, $data['table_filter'])) { 260 continue; 261 } 262 $renderer->tablerow_open(); 263 if(!is_array($set)) { 264 $set = array($set); 265 } 266 foreach($set as $value) { 267 $renderer->tablecell_open(); 268 $this->render_var($renderer, $value, null, 1); 269 $renderer->tablecell_close(); 270 } 271 $renderer->tablerow_close(); 272 } 273 $renderer->tabletbody_close(); 274 $renderer->table_close(); 275 break; 276 277 278 case 'table_with_header': 279 if(!is_array($var)) { 280 $var = array($var); 281 } 282 $table = array(); 283 //get table rows 284 foreach($var as $set) { 285 //apply filter 286 if(isset($data['table_filter']) && !$json_o->filter($set, $data['table_filter'])) { 287 continue; 288 } 289 290 //get table header on the first pass 291 if(!isset($header)) { 292 $header = array(); 293 // if not specified, generate header automatically 294 if($data['table_header'] === '') { 295 $table_header = array(); 296 if(is_array($set)) { 297 foreach($set as $key => $dummy) { 298 $table_header[$key] = array($key); 299 } 300 } 301 $data['table_header'] = $table_header; 302 } 303 foreach($data['table_header'] as $key => $tokens) { 304 if(strpos($key, '_tooltip_') !== 0) { 305 $header[] = $key; 306 } 307 } 308 } 309 310 //get cells for one row 311 $row = array(); 312 $tooltips = array(); 313 foreach($data['table_header'] as $name => $tokens) { 314 $v = $set; 315 //get value of the variable 316 foreach($tokens as $tok) { 317 if(is_array($v) && isset($v[$tok])) { 318 $v = $v[$tok]; 319 } 320 else { 321 $v = null; 322 break; 323 } 324 } 325 326 //If $name begins with '_tooltip_', it will display only as tooltip 327 if(strpos($name, '_tooltip_') === 0) { 328 $tooltips[substr($name, 9)] = $v; 329 } 330 else { 331 $row[$name] = $v; 332 } 333 } 334 $row['_tooltip_'] = $tooltips; 335 $table[] = $row; 336 } 337 338 //render table 339 $renderer->table_open(); 340 341 $renderer->tablethead_open(); 342 $renderer->tablerow_open(); 343 foreach($header as $value) { 344 $renderer->tableheader_open(); 345 $this->render_var($renderer, $value, null, 1); 346 $renderer->tableheader_close(); 347 } 348 $renderer->tablerow_close(); 349 $renderer->tablethead_close(); 350 351 $renderer->tabletbody_open(); 352 foreach($table as $set) { 353 $tooltips = $set['_tooltip_']; 354 unset($set['_tooltip_']); 355 356 //open table row, optionally with tooltip 357 if(isset($tooltips[''])) { 358 $renderer->doc .= DOKU_TAB.'<tr title="'.htmlspecialchars($tooltips['']).'">'.DOKU_LF.DOKU_TAB.DOKU_TAB; 359 } 360 else { 361 $renderer->tablerow_open(); 362 } 363 364 //render all cells in row, some may have tooltips, some may have custom format 365 foreach($set as $name => $value) { 366 if(isset($tooltips[$name])) { 367 $renderer->doc .= '<td title="'.htmlspecialchars($tooltips[$name]).'">'; 368 } 369 else { 370 $renderer->tablecell_open(); 371 } 372 $this->render_var($renderer, 373 $value, 374 isset($data['format'][$name]) ? $data['format'][$name] : null, 375 1); 376 $renderer->tablecell_close(); 377 } 378 $renderer->tablerow_close(); 379 } 380 $renderer->tabletbody_close(); 381 382 $renderer->table_close(); 383 break; 384 385 386 default: 387 $renderer->cdata($data['match']); 388 break; 389 } 390 return true; 391 } 392 393 return false; 394 } 395 396 397 /** 398 * Render the variable 399 * 400 * @param Doku_Renderer $renderer The renderer 401 * @param mixed $var variable to be rendered. If array, then members will be rendered. 402 * @param array $format render variable in specific format. 403 * @param integer $recursive if >1 and $var is array, then array elements will be rendered 404 * 405 * @return integer number of elements rendered 406 */ 407 private function render_var(Doku_Renderer $renderer, $var, $format=null, $recursive=0, $print_separator=false) { 408 $i = $iprev = 0; 409 if(is_array($format) && ($format['func'] === 'format_code' || $format['func'] === 'format_ejs')) { 410 call_user_func(array($this, $format['func']), $renderer, $var, $format['param']); 411 } 412 else if(is_scalar($var)) { 413 if($print_separator) { 414 $renderer->doc .= ', '; 415 } 416 if(is_bool($var)) { 417 $renderer->cdata($this->getConf($var ? 'true_str' : 'false_str')); 418 } 419 else if(is_string($var) && is_array($format)) { 420 call_user_func(array($this, $format['func']), $renderer, $var, $format['param']); 421 } 422 else { 423 $renderer->cdata($var); 424 } 425 $i++; 426 } 427 else if(is_array($var)) { 428 if($recursive > 0) { 429 foreach($var as $v) { 430 if($i > $iprev) { 431 $print_separator = true; 432 $iprev = $i; 433 } 434 $i += $this->render_var($renderer, $v, $format, $recursive-1, $print_separator); 435 } 436 if($i === 0 && count($var) > 0) { 437 $renderer->cdata($this->getConf('array_str')); 438 } 439 } 440 } 441 else { 442 $renderer->cdata($this->getConf('null_str')); 443 $i++; 444 } 445 446 return $i; 447 } 448 449 450 /** 451 * Handle format parameter 452 * 453 * @param string $format format parameter from json data extractor 454 * 455 * @return empty string or array with function name and parameter, which 456 * renders the variable according to format. 457 */ 458 private function handle_format($format) { 459 $param = null; 460 461 // Dokuwiki Header 462 // #header5# 463 if(substr($format, 0, 6) === 'header') { 464 //get header level 465 $param = intval(substr($format, 6)); 466 if($param < 1) $param = 1; 467 else if($param > 5) $param = 5; 468 $format = 'header'; 469 } 470 471 // Dokuwiki media internal or external link 472 // #media?L200x300# 473 else if(substr($format, 0, 5) === 'media') { 474 list($format, $align_size) = array_pad(explode('?', $format, 2), 2, ''); 475 if($align_size === '') { 476 $param = array(null, null, null, null, null); 477 } 478 else if($align_size === 'linkonly') { 479 $param = array(null, null, null, null, 'linkonly'); 480 } 481 else { 482 $align = substr($align_size, 0, 1); 483 if ($align === 'l') $align = 'left'; 484 else if($align === 'c') $align = 'center'; 485 else if($align === 'r') $align = 'right'; 486 else $align = null; 487 list($width, $height) = array_pad(explode('x', substr($align_size, 1), 2), 2, ''); 488 $width = intval($width); 489 if($width > 0) { 490 $height = intval($height); 491 if($height <= 0) { 492 $height = null; 493 } 494 } 495 else { 496 $width = $height = null; 497 } 498 $param = array($align, $width, $height, null, null); 499 } 500 } 501 502 // RSS 503 // #rss?n?nosort?reverse?author?date?details# 504 else if(substr($format, 0, 3) === 'rss') { 505 $param = array(); 506 if(preg_match('/\b(\d+)\b/', $format, $match)) { 507 $param['max'] = $match[1]; 508 } 509 else { 510 $param['max'] = 8; 511 } 512 $param['reverse'] = preg_match('/\brev/', $format); 513 $param['author'] = preg_match('/\b(by|author)/', $format); 514 $param['date'] = preg_match('/\bdate/', $format); 515 $param['details'] = preg_match('/\b(desc|detail)/', $format); 516 $param['nosort'] = preg_match('/\bnosort/', $format); 517 $format = 'rss'; 518 } 519 520 // EJS template 521 // %$#ejs?template% 522 if(substr($format, 0, 4) === 'ejs?') { 523 524 $patterns = array ('/%/', '/#/', '/:/', '/,/', '/<\$=/', '/\$>/'); 525 $replace = array ('%', '#', ':', ',', '<%=', '%>'); 526 527 //get template and replace some patterns 528 $param = preg_replace($patterns, $replace, substr($format, 4)); 529 $format = 'ejs'; 530 } 531 532 //other formats don't need special handler 533 534 //get format_function name 535 $func = 'format_'.$format; 536 537 return method_exists($this, $func) ? 538 array('func' => $func, 'param' => $param) : 539 ''; 540 } 541 542 543 /** 544 * Renderers for different formats 545 * 546 * @param Doku_Renderer $renderer The renderer 547 * @param string $var variable to render 548 * @param mixed $param additional parameter 549 */ 550 // Dokuwiki Title 551 private function format_header(Doku_Renderer $renderer, $var, $param) { 552 $renderer->header($var, $param, 0); 553 } 554 555 // \\server\share|Title 556 // https://example.com|Title 557 // dokuwiki:link|Title 558 private function format_link(Doku_Renderer $renderer, $var, $param) { 559 list($id, $title) = array_pad(explode('|', $var, 2), 2, ''); 560 if(strpos($id, '\\') === 0) { 561 $renderer->windowssharelink($id, $title==='' ? $id : $title); 562 } 563 else if(strpos($id, '://')) { 564 $renderer->externallink($id, $title==='' ? $id : $title); 565 } 566 else if(strlen($id) > 0) { 567 if($title === '') { 568 $tok = explode(':', $id); 569 $last = $tok[count($tok) - 1]; 570 $title = $last ? $last : $id; 571 } 572 $renderer->internallink($id, $title); 573 } 574 } 575 576 // https://example.com/media.png|Title 577 // dokuwiki:link.png|Title 578 private function format_media(Doku_Renderer $renderer, $var, $param) { 579 list($id, $title) = array_pad(explode('|', $var, 2), 2, ''); 580 if($title === '') { 581 $title = $id; 582 } 583 if(strpos($id, '://')) { 584 $renderer->externalmedia($id, $title, $param[0], $param[1], $param[2], $param[3], $param[4]); 585 } 586 else { 587 $renderer->internalmedia($id, $title, $param[0], $param[1], $param[2], $param[3], $param[4]); 588 } 589 } 590 591 // name@example.com|Title 592 private function format_email(Doku_Renderer $renderer, $var, $param) { 593 list($id, $title) = array_pad(explode('|', $var, 2), 2, ''); 594 if($title === '') { 595 $title = $id; 596 } 597 $renderer->emaillink($id, $title); 598 } 599 600 // http://slashdot.org/index.rss 601 private function format_rss(Doku_Renderer $renderer, $var, $param) { 602 $renderer->rss($var, $param); 603 } 604 605 // json_encode 606 private function format_code(Doku_Renderer $renderer, $var, $param) { 607 $renderer->doc .= '<pre class="json-extract-code">' 608 .htmlspecialchars(json_encode($var, JSON_PRETTY_PRINT|JSON_UNESCAPED_UNICODE)) 609 .'</pre>'; 610 } 611 612 // EJS template https://ejs.co/ 613 // Javascript will take json data and template from hidden divs, then will generate output 614 private function format_ejs(Doku_Renderer $renderer, $var, $param) { 615 $renderer->doc .= '<span class="json-extract-ejs"><span id="data">' 616 .htmlspecialchars(json_encode($var)) 617 .'</span><span id="template">' 618 .htmlspecialchars($param) 619 .'</span></span>'; 620 } 621} 622