1<?php 2 3/** 4 * Info Indexmenu: Show a customizable and sortable index for a namespace. 5 * 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author Samuele Tognini <samuele@samuele.netsons.org> 8 * 9 */ 10 11use dokuwiki\Extension\SyntaxPlugin; 12use dokuwiki\File\PageResolver; 13use dokuwiki\plugin\indexmenu\Search; 14use dokuwiki\Ui\Index; 15 16/** 17 * All DokuWiki plugins to extend the parser/rendering mechanism 18 * need to inherit from this class 19 */ 20class syntax_plugin_indexmenu_indexmenu extends SyntaxPlugin 21{ 22 /** 23 * What kind of syntax are we? 24 */ 25 public function getType() 26 { 27 return 'substition'; 28 } 29 30 /** 31 * Behavior regarding the paragraph 32 */ 33 public function getPType() 34 { 35 return 'block'; 36 } 37 38 /** 39 * Where to sort in? 40 */ 41 public function getSort() 42 { 43 return 138; 44 } 45 46 /** 47 * Connect pattern to lexer 48 * 49 * @param string $mode 50 */ 51 public function connectTo($mode) 52 { 53 $this->Lexer->addSpecialPattern('{{indexmenu>.+?}}', $mode, 'plugin_indexmenu_indexmenu'); 54 } 55 56 /** 57 * Handler to prepare matched data for the rendering process 58 * 59 * @param string $match The text matched by the patterns 60 * @param int $state The lexer state for the match 61 * @param int $pos The character position of the matched text 62 * @param Doku_Handler $handler The Doku_Handler object 63 * @return array Return an array with all data you want to use in render 64 * 65 * @throws Exception 66 */ 67 public function handle($match, $state, $pos, Doku_Handler $handler) 68 { 69 $theme = 'default'; // name of theme for images and additional css 70 $level = -1; // requested depth of initial opened nodes, -1:all 71 $max = 0; // number of levels loaded initially, rest should be loaded with ajax. (TODO actual default is 1) 72 $maxAjax = 1; // number of levels loaded per ajax request 73 $subNSs = []; 74 $skipNsCombined = []; 75 $skipFileCombined = []; 76 $skipNs = ''; 77 $skipFile = ''; 78 /* @deprecated 2022-04-15 dTree only */ 79 $maxJs = 1; 80 /* @deprecated 2022-04-15 dTree only. Fancytree always random id */ 81 $gen_id = 'random'; 82 /* @deprecated 2021-07-01 -- allow (temporary) switching between versions of the js treemenu */ 83 $jsVersion = 1; // 0:both, 1:dTree, 2:Fancytree 84 /* @deprecated 2022-04-15 dTree only */ 85 $jsAjax = ''; 86 87 $defaultsStr = $this->getConf('defaultoptions'); 88 $defaults = explode(' ', $defaultsStr); 89 90 $match = substr($match, 12, -2); 91 //split namespace,level,theme 92 [$nsStr, $optsStr] = array_pad(explode('|', $match, 2), 2, ''); 93 //split options 94 $opts = explode(' ', $optsStr); 95 96 //Context option 97 $context = $this->hasOption($defaults, $opts, 'context'); 98 99 //split subnamespaces with their level of open/closed nodes 100 // PREG_SPLIT_NO_EMPTY flag filters empty pieces e.g. due to multiple spaces 101 $nsStrs = preg_split("/ /u", $nsStr, -1, PREG_SPLIT_NO_EMPTY); 102 //skips i=0 because that becomes main $ns 103 $counter = count($nsStrs); 104 //skips i=0 because that becomes main $ns 105 for ($i = 1; $i < $counter; $i++) { 106 $subns_lvl = explode("#", $nsStrs[$i]); 107 //context should parse this later in correct context 108 if (!$context) { 109 $subns_lvl[0] = $this->parseNs($subns_lvl[0]); 110 } 111 $subNSs[] = [ 112 $subns_lvl[0], //subns 113 isset($subns_lvl[1]) && is_numeric($subns_lvl[1]) ? $subns_lvl[1] : -1 // level 114 ]; 115 } 116 //empty pieces were filtered 117 if ($nsStrs === []) { 118 $nsStrs[0] = ''; 119 } 120 //split main requested namespace 121 if (preg_match('/(.*)#(\S*)/u', $nsStrs[0], $matched_ns_lvl)) { 122 //split level 123 $ns = $matched_ns_lvl[1]; 124 if (is_numeric($matched_ns_lvl[2])) { 125 $level = (int)$matched_ns_lvl[2]; 126 } 127 } else { 128 $ns = $nsStrs[0]; 129 } 130 //context needs to be resolved later 131 if (!$context) { 132 $ns = $this->parseNs($ns); 133 } 134 135 //nocookie option (disable for uncached pages) 136 /* @deprecated 2023-11 dTree only?, too complex */ 137 $nocookie = $context || $this->hasOption($defaults, $opts, 'nocookie'); 138 //noscroll option 139 /** @deprecated 2023-11 dTree only and too complex */ 140 $noscroll = $this->hasOption($defaults, $opts, 'noscroll'); 141 //Open at current namespace option 142 $navbar = $this->hasOption($defaults, $opts, 'navbar'); 143 //no namespaces options 144 $nons = $this->hasOption($defaults, $opts, 'nons'); 145 //no pages option 146 $nopg = $this->hasOption($defaults, $opts, 'nopg'); 147 //disable toc preview 148 $notoc = $this->hasOption($defaults, $opts, 'notoc'); 149 //disable the right context menu 150 $nomenu = $this->hasOption($defaults, $opts, 'nomenu'); 151 //Main sort method 152 $tsort = $this->hasOption($defaults, $opts, 'tsort'); 153 $dsort = $this->hasOption($defaults, $opts, 'dsort'); 154 if ($tsort) { 155 $sort = 't'; 156 } elseif ($dsort) { 157 $sort = 'd'; 158 } else { 159 $sort = 0; 160 } 161 //sort directories in the same way as files 162 $nsort = $this->hasOption($defaults, $opts, 'nsort'); 163 //sort headpages up 164 $hsort = $this->hasOption($defaults, $opts, 'hsort'); 165 //Metadata sort method 166 if ($msort = $this->hasOption($defaults, $opts, 'msort')) { 167 $msort = 'indexmenu_n'; 168 } elseif ($value = $this->getOption($defaultsStr, $optsStr, '/msort#(\S+)/u')) { 169 $msort = str_replace(':', ' ', $value); 170 } 171 //reverse sort 172 $rsort = $this->hasOption($defaults, $opts, 'rsort'); 173 174 if ($sort) $jsAjax .= "&sort=" . $sort; 175 if ($msort) $jsAjax .= "&msort=" . $msort; 176 if ($rsort) $jsAjax .= "&rsort=1"; 177 if ($nsort) $jsAjax .= "&nsort=1"; 178 if ($hsort) $jsAjax .= "&hsort=1"; 179 if ($nopg) $jsAjax .= "&nopg=1"; 180 181 //javascript option 182 $dir = ''; 183 //check defaults for js,js#theme, #theme 184 if (!$js = in_array('js', $defaults)) { 185 if (preg_match('/(?:^|\s)(js)?#(\S*)/u', $defaultsStr, $matched_js_theme) > 0) { 186 if (!empty($matched_js_theme[1])) { 187 $js = true; 188 } 189 if (isset($matched_js_theme[2])) { 190 $dir = $matched_js_theme[2]; 191 } 192 } 193 } 194 //check opts for nojs,#theme or js,js#theme 195 if ($js) { 196 if (in_array('nojs', $opts)) { 197 $js = false; 198 } elseif (preg_match('/(?:^|\s)(?:js)?#(\S*)/u', $optsStr, $matched_theme) > 0) { 199 if (isset($matched_theme[1])) { 200 $dir = $matched_theme[1]; 201 } 202 } 203 } elseif ($js = in_array('js', $opts)) { 204 //use theme from the defaults 205 } elseif (preg_match('/(?:^|\s)js#(\S*)/u', $optsStr, $matched_theme) > 0) { 206 $js = true; 207 if (isset($matched_theme[1])) { 208 $dir = $matched_theme[1]; 209 } 210 } 211 212 if ($js) { 213 //exist theme? 214 if (!empty($dir) && is_dir(DOKU_PLUGIN . "indexmenu/images/" . $dir)) { 215 $theme = $dir; 216 } 217 218 //id generation method 219 /* @deprecated 2023-11 not needed anymore */ 220 $gen_id = $this->getOption($defaultsStr, $optsStr, '/id#(\S+)/u'); 221 222 //max option: #n is no of lvls during initialization , #m levels retrieved per ajax request 223 $matchPattern = '/max#(\d+)(?:$|\s+|#(\d+))/u'; 224 if ($matched_lvl_sublvl = $this->getOption($defaultsStr, $optsStr, $matchPattern, true)) { 225 $max = $matched_lvl_sublvl[1]; 226 if (!empty($matched_lvl_sublvl[2])) { 227 $jsAjax .= "&max=" . $matched_lvl_sublvl[2]; 228 $maxAjax = (int)$matched_lvl_sublvl[2]; 229 } 230 //disable cookie to avoid javascript errors 231 $nocookie = true; 232 } else { 233 $max = 0; //todo current default seems 1. 234 } 235 236 //max js option 237 if ($maxjs_lvl = $this->getOption($defaultsStr, $optsStr, '/maxjs#(\d+)/u')) { 238 $maxJs = $maxjs_lvl; 239 } 240 /* @deprecated 2021-07-01 -- allow (temporary) switching between versions of the js treemenu */ 241 $treeNew = $this->hasOption($defaults, $opts, 'treenew'); //overrides old and both 242 /* @deprecated 2021-07-01 -- allow (temporary) switching between versions of the js treemenu */ 243 $treeOld = $this->hasOption($defaults, $opts, 'treeold'); //overrides both 244 /* @deprecated 2021-07-01 -- allow (temporary) switching between versions of the js treemenu */ 245 $treeBoth = $this->hasOption($defaults, $opts, 'treeboth'); 246// $jsVersion = $treeNew ? 2 : ($treeOld ? 1 : ($treeBoth ? 0 : $jsVersion)); 247 $jsVersion = $treeOld ? 1 : ($treeNew ? 2 : ($treeBoth ? 0 : $jsVersion)); 248// error_log('$treeOld:'.$treeOld.'$treeNew:'.$treeNew.'$treeBoth:'.$treeBoth); 249 250 if ($jsVersion !== 1) { 251 //check for theme of fancytree (overrides old dTree theme eventually?) 252 if (!empty($dir) && is_dir(DOKU_PLUGIN . 'indexmenu/scripts/fancytree/skin-' . $dir)) { 253 $theme = $dir; 254 } 255 // $theme='default' is later overwritten by 'win7' 256 } 257 } 258 if (is_numeric($gen_id)) { 259 /* @deprecated 2023-11 not needed anymore */ 260 $identifier = $gen_id; 261 } elseif ($gen_id == 'ns') { 262 $identifier = sprintf("%u", crc32($ns)); 263 } else { 264 $identifier = uniqid(random_int(0, mt_getrandmax())); 265 } 266 267 //skip namespaces in index 268 $skipNsCombined[] = $this->getConf('skip_index'); 269 if (preg_match('/skipns[+=](\S+)/u', $optsStr, $matched_skipns) > 0) { 270 //first sign is: '+' (parallel to conf) or '=' (replace conf) 271 $action = $matched_skipns[0][6]; 272 $index = 0; 273 if ($action == '+') { 274 $index = 1; 275 } 276 //directly used in search 277 $skipNsCombined[$index] = $matched_skipns[1]; 278 //fancytree 279 $skipNs = ($action == '+' ? '+' : '=') . $matched_skipns[1]; 280 //dTree 281 $jsAjax .= "&skipns=" . utf8_encodeFN(($action == '+' ? '+' : '=') . $matched_skipns[1]); 282 } 283 //skip file 284 $skipFileCombined[] = $this->getConf('skip_file'); 285 if (preg_match('/skipfile[+=](\S+)/u', $optsStr, $matched_skipfile) > 0) { 286 //first sign is: '+' (parallel to conf) or '=' (replace conf) 287 $action = $matched_skipfile[0][8]; 288 $index = 0; 289 if ($action == '+') { 290 $index = 1; 291 } 292 //directly used in search 293 $skipFileCombined[$index] = $matched_skipfile[1]; 294 //fancytree 295 $skipFile = ($action == '+' ? '+' : '=') . $matched_skipfile[1]; 296 //dTree 297 $jsAjax .= "&skipfile=" . utf8_encodeFN(($action == '+' ? '+' : '=') . $matched_skipfile[1]); 298 } 299 300 //js options 301 return [ 302 $ns, //0 303 [ //1=js_dTreeOpts 304 'theme' => $theme, 305 'identifier' => $identifier, //deprecated 306 'nocookie' => $nocookie, //deprecated 307 'navbar' => $navbar, 308 'noscroll' => $noscroll, //deprecated 309 'maxJs' => $maxJs, //deprecated 310 'notoc' => $notoc, //will be changed to default notoc 311 'jsAjax' => $jsAjax, //deprecated 312 'context' => $context, //only in handler()? 313 'nomenu' => $nomenu //will be changed to default nomenu 314 ], 315 [ //2=sort 316 'sort' => $sort, 317 'msort' => $msort, 318 'rsort' => $rsort, 319 'nsort' => $nsort, 320 'hsort' => $hsort, 321 ], 322 [ //3=opts 323 'level' => $level, // requested depth of initial opened nodes, -1:all 324 'nons' => $nons, 325 'nopg' => $nopg, 326 'subnss' => $subNSs, //only used for initial load 327 'navbar' => $navbar, //add current ns to subNSs, for initial load 328 'max' => $max, //number of levels loaded initially, rest should be loaded with ajax 329 'maxajax' => $maxAjax, //number of levels loaded per ajax request 330 'js' => $js, 331 'skipnscombined' => $skipNsCombined, 332 'skipfilecombined' => $skipFileCombined, 333 'skipns' => $skipNs, 334 'skipfile' => $skipFile, 335 'headpage' => $this->getConf('headpage'), 336 'hide_headpage' => $this->getConf('hide_headpage'), 337 'theme' => $theme 338 ], 339 $jsVersion //4 340 ]; 341 } 342 343 /** 344 * Looks if the default options and syntax options has the requested option 345 * 346 * @param array $defaultsOpts array of default options 347 * @param array $opts array of options provided via syntax 348 * @param string $optionName name of requested option 349 * @return bool has $optionName? 350 */ 351 private function hasOption($defaultsOpts, $opts, $optionName) 352 { 353 $name = $optionName; 354 if (substr($optionName, 0, 2) == 'no') { 355 $inverseName = substr($optionName, 2); 356 } else { 357 $inverseName = 'no' . $optionName; 358 } 359 360 if (in_array($name, $defaultsOpts)) { 361 return !in_array($inverseName, $opts); 362 } else { 363 return in_array($name, $opts); 364 } 365 } 366 367 /** 368 * Looks for the value of the requested option in the default options and syntax options 369 * 370 * @param string $defaultsString default options string 371 * @param string $optsString syntax options string 372 * @param string $matchPattern pattern to search for 373 * @param bool $multipleMatches if multiple returns array, otherwise the first match 374 * @return string|array 375 */ 376 private function getOption($defaultsString, $optsString, $matchPattern, $multipleMatches = false) 377 { 378 if (preg_match($matchPattern, $optsString, $match_o) > 0) { 379 if ($multipleMatches) { 380 return $match_o; 381 } else { 382 return $match_o[1]; 383 } 384 } elseif (preg_match($matchPattern, $defaultsString, $match_d) > 0) { 385 if ($multipleMatches) { 386 return $match_d; 387 } else { 388 return $match_d[1]; 389 } 390 } 391 return false; 392 } 393 394 /** 395 * Handles the actual output creation. 396 * 397 * @param string $format output format being rendered 398 * @param Doku_Renderer $renderer the current renderer object 399 * @param array $data data created by handler() 400 * @return boolean rendered correctly? 401 */ 402 public function render($format, Doku_Renderer $renderer, $data) 403 { 404 global $ACT; 405 global $conf; 406 global $INFO; 407 408 $ns = $data[0]; 409 //theme, identifier, nocookie, navbar, noscroll, maxJs, notoc, jsAjax, context, nomenu 410 $js_dTreeOpts = $data[1]; 411 //sort, msort, rsort, nsort, hsort 412 $sort = $data[2]; 413 //opts for search(): level, nons, nopg, subnss, max, maxajax, js, skipns, skipfile, skipnscombined, 414 //skipfilecombined, headpage, hide_headpage 415 $opts = $data[3]; 416 /* @deprecated 2021-07-01 temporary */ 417 $jsVersion = $data[4]; 418 419 if ($format == 'xhtml') { 420 if ($ACT == 'preview') { 421 //Check user permission to display indexmenu in a preview page 422 if ( 423 $this->getConf('only_admins') && 424 $conf['useacl'] && 425 $INFO['perm'] < AUTH_ADMIN 426 ) { 427 return false; 428 } 429 //disable cookies 430 $js_dTreeOpts['nocookie'] = true; 431 } 432 if ($opts['js'] & $conf['defer_js']) { 433 msg( 434 'Indexmenu Plugin: If you use the \'js\'-option of the indexmenu plugin, you have to ' 435 . 'disable the <a href="https://www.dokuwiki.org/config:defer_js">\'defer_js\'</a>-setting. ' 436 . 'This setting is temporary, in the future the indexmenu plugin will be improved.', 437 -1 438 ); 439 } 440 //Navbar with nojs 441 if ($js_dTreeOpts['navbar'] && !$opts['js']) { 442 if (!isset($ns)) { 443 $ns = ':'; 444 } 445 //add ns of current page to let open these nodes (within the $ns), open only 1 level. 446 $currentNS = getNS($INFO['id']); 447 if ($currentNS !== false) { 448 $opts['subnss'][] = [$currentNS, 1]; 449 } 450 $renderer->info['cache'] = false; 451 } 452 if ($js_dTreeOpts['context']) { 453 //resolve ns and subns's relative to current wiki page (instead of sidebar) 454 $ns = $this->parseNs($ns, $INFO['id']); 455 foreach ($opts['subnss'] as $key => $value) { 456 $opts['subnss'][$key][0] = $this->parseNs($value[0], $INFO['id']); 457 } 458 $renderer->info['cache'] = false; 459 } 460 //build index 461 $html = $this->buildHtmlIndexmenu($ns, $js_dTreeOpts, $sort, $opts, $jsVersion); 462 //alternative if empty 463 if (!@$html) { 464 $html = $this->getConf('empty_msg'); 465 $html = str_replace('{{ns}}', cleanID($ns), $html); 466 $html = p_render('xhtml', p_get_instructions($html), $info); 467 } 468 $renderer->doc .= $html; 469 return true; 470 } elseif ($format == 'metadata') { 471 /** @var Doku_Renderer_metadata $renderer */ 472 if (!($js_dTreeOpts['navbar'] && !$opts['js']) && !$js_dTreeOpts['context']) { 473 //this is an indexmenu page that needs the PARSER_CACHE_USE event trigger; 474 $renderer->meta['indexmenu']['hasindexmenu'] = true; 475 } 476 //summary 477 $renderer->doc .= (empty($ns) ? $conf['title'] : nons($ns)) . " index\n\n"; 478 unset($renderer->persistent['indexmenu']); 479 return true; 480 } else { 481 return false; 482 } 483 } 484 485 /** 486 * Return the index 487 * 488 * @param string $ns 489 * @param array $js_dTreeOpts entries: theme, identifier, nocookie, navbar, noscroll, maxJs, notoc, jsAjax, context, 490 * nomenu 491 * @param array $sort entries: sort, msort, rsort, nsort, hsort 492 * @param array $opts entries of opts for search(): level, nons, nopg, nss, max, maxajax, js, skipns, skipfile, 493 * skipnscombined, skipfilecombined, headpage, hide_headpage 494 * @param int $jsVersion 495 * @return bool|string return html for a nojs index and when enabled the js rendered index, otherwise false 496 * 497 * @author Samuele Tognini <samuele@samuele.netsons.org> 498 */ 499 private function buildHtmlIndexmenu($ns, $js_dTreeOpts, $sort, $opts, $jsVersion) 500 { 501 $js_name = "indexmenu_" . $js_dTreeOpts['identifier']; 502 //TODO temporary hack, to switch in Search between searchIndexmenuItemsNew() and searchIndexmenuItems() 503 $opts['tempNew'] = false; 504 $search = new Search($sort); 505 $nodes = $search->search($ns, $opts); 506 507 if (!$nodes) return false; 508 509 // javascript index 510 $output_js = ''; 511 if ($opts['js']) { 512 $ns = str_replace('/', ':', $ns); 513 514 // $jsversion: 0:both, 1:dTree, 2:Fancytree 515 if ($jsVersion < 2) { 516 $output_js .= $this->builddTree($nodes, $ns, $js_dTreeOpts, $js_name, $opts['max']); 517 } 518 if ($jsVersion !== 1) { 519 $output_js .= $this->buildFancyTree($js_name, $ns, $opts, $sort); 520 } 521 522 //remove unwanted nodes from standard index 523 $this->cleanNojsData($nodes); 524 } 525 $output = "\n"; 526 $output .= $this->buildNoJSTree($nodes, $js_name, $js_dTreeOpts['jsAjax']); 527 $output .= $output_js; 528 return $output; 529 } 530 531 private function buildNoJSTree($nodes, $js_name, $jsAjax) 532 { 533 // Nojs dokuwiki index 534 // extra div needed when index is first element in sidebar of dokuwiki template, template uses this to 535 // toggle sidebar the toggle interacts with hide needed for js option. 536 $idx = new Index(); 537 return '<div>' 538 . '<div id="nojs_' . $js_name . '" data-jsajax="' . utf8_encodeFN($jsAjax) . '" class="indexmenu_nojs">' 539 . html_buildlist($nodes, 'idx', [$this, 'formatIndexmenuItem'], [$idx, 'tagListItem']) 540 . '</div>' 541 . '</div>'; 542 } 543 544 private function buildFancyTree($js_name, $ns, $opts, $sort) 545 { 546 global $conf; 547 //not needed, because directly retrieved from config 548 unset($opts['headpage']); 549 unset($opts['hide_headpage']); 550 unset($opts['js']); //always true 551 unset($opts['skipnscombined']); 552 unset($opts['skipfilecombined']); 553 554 /* @deprecated 2023-08-14 remove later */ 555 if ($opts['theme'] == 'default') { 556 $opts['theme'] = 'win7'; 557 } 558 $options = [ 559 'ns' => $ns, 560 'opts' => $opts, 561 'sort' => $sort, 562 'contextmenu' => false, 563 'startpage' => $conf['start'] //needed? or for contextmenu? 564 ]; 565 return '<div id="tree2_' . $js_name . '" class="indexmenu_js2 skin-' . $opts['theme'] . '"' 566 . 'data-options=\'' . json_encode($options) . '\'></div>'; 567 } 568 569 /** 570 * Build the browsable index of pages using javascript 571 * 572 * @param array $nodes array with items of the tree 573 * @param string $ns requested namespace 574 * @param array $js_dTreeOpts options for javascript renderer 575 * @param string $js_name identifier for this index 576 * @param int $max the node at $max level will retrieve all its child nodes through the AJAX mechanism 577 * @return bool|string returns inline javascript or false 578 * 579 * @author Samuele Tognini <samuele@samuele.netsons.org> 580 * @author Rene Hadler 581 * 582 * @deprecated 2023-11 will be replace by Fancytree 583 */ 584 private function builddTree($nodes, $ns, $js_dTreeOpts, $js_name, $max) 585 { 586 global $conf; 587 $hns = false; 588 if (empty($nodes)) { 589 return false; 590 } 591 592//TODO jsAjax is empty?? while max is set to 1 593 // Render requested ns as root 594 $headpage = $this->getConf('headpage'); 595 // if rootnamespace and headpage, then add startpage as headpage 596 // TODO seems not logic, when desired use $conf[headpage]=:start: ?? 597 if (empty($ns) && !empty($headpage)) { 598 $headpage .= ',' . $conf['start']; 599 } 600 $title = Search::getNamespaceTitle($ns, $headpage, $hns); 601 if (empty($title)) { 602 if (empty($ns)) { 603 $title = hsc($conf['title']); 604 } else { 605 $title = $ns; 606 } 607 } 608 // inline javascript 609 $out = "<script type='text/javascript'>\n"; 610 $out .= "<!--//--><![CDATA[//><!--\n"; 611 $out .= "var $js_name = new dTree('" . $js_name . "','" . $js_dTreeOpts['theme'] . "');\n"; 612 //javascript config options 613 $sepchar = idfilter(':', false); 614 $out .= "$js_name.config.urlbase='" . substr(wl(":"), 0, -1) . "';\n"; 615 $out .= "$js_name.config.sepchar='" . $sepchar . "';\n"; 616 if ($js_dTreeOpts['notoc']) { 617 $out .= "$js_name.config.toc=false;\n"; 618 } 619 if ($js_dTreeOpts['nocookie']) { 620 $out .= "$js_name.config.useCookies=false;\n"; 621 } 622 if ($js_dTreeOpts['noscroll']) { 623 $out .= "$js_name.config.scroll=false;\n"; 624 } 625 //1 is default in dTree 626 if ($js_dTreeOpts['maxJs'] > 1) { 627 $out .= "$js_name.config.maxjs=" . $js_dTreeOpts['maxJs'] . ";\n"; 628 } 629 if (!empty($js_dTreeOpts['jsAjax'])) { 630 $out .= "$js_name.config.jsajax='" . utf8_encodeFN($js_dTreeOpts['jsAjax']) . "';\n"; 631 } 632 633 //add root node 634 $out .= $js_name . ".add('" . idfilter(cleanID($ns), false) . "',0,-1," . json_encode($title); 635 if ($hns) { 636 $out .= ",'" . idfilter(cleanID($hns), false) . "'"; 637 } 638 $out .= ");\n"; 639 //add nodes 640 [$nodesArray, $openNodes] = $this->builddTreeNodes($nodes, $js_name); 641 $out .= $nodesArray; 642 //write to document 643 $out .= "document.write(" . $js_name . ");\n"; 644 //initialize index 645 $out .= "jQuery(function(){" . $js_name . ".init("; 646 $out .= (int)is_file(DOKU_PLUGIN . 'indexmenu/images/' . $js_dTreeOpts['theme'] . '/style.css') . ","; 647 $out .= (int)$js_dTreeOpts['nocookie'] . ","; 648 $out .= '"' . $openNodes . '",'; 649 $out .= (int)$js_dTreeOpts['navbar'] . ","; 650 $out .= (int)$max; 651 if ($js_dTreeOpts['nomenu']) { 652 $out .= ",1"; 653 } 654 $out .= ");});\n"; 655 656 $out .= "//--><!]]>\n"; 657 $out .= "</script>\n"; 658 return $out; 659 } 660 661 /** 662 * Return array of javascript nodes and nodes to open. 663 * 664 * @param array $nodes array with items of the tree 665 * @param string $js_name identifier for this index 666 * @param boolean $noajax return as inline js (=true) or array for ajax response (=false) 667 * @return array|bool returns array with 668 * - a string of the javascript nodes 669 * - and a string of space separated numbers of the opened nodes 670 * or false when no data provided 671 * 672 * @author Samuele Tognini <samuele@samuele.netsons.org> 673 * 674 * @deprecated 2023-11 will be replace by Fancytree 675 */ 676 public function builddTreeNodes($nodes, $js_name, $noajax = true) 677 { 678 if (empty($nodes)) { 679 return false; 680 } 681 //Array of nodes to check 682 $q = ['0']; 683 //Current open node 684 $currentOpenNode = 0; 685 $out = ''; 686 $openNodes = ''; 687 if ($noajax) { 688 $jscmd = $js_name . ".add"; 689 $separator = ";\n"; 690 } else { 691 $jscmd = "new Array "; 692 $separator = ","; 693 } 694 695 foreach ($nodes as $i => $node) { 696 $i++; 697 //Remove already processed nodes (greater level = lower level) 698 while (isset($nodes[end($q) - 1]) && $node['level'] <= $nodes[end($q) - 1]['level']) { 699 array_pop($q); 700 } 701 702 //till i found its father node 703 if ($node['level'] == 1) { 704 //root node 705 $father = '0'; 706 } else { 707 //Father node 708 $father = end($q); 709 } 710 //add node and its options 711 if ($node['type'] == 'd') { 712 //Search the lowest open node of a tree branch in order to open it. 713 if ($node['open']) { 714 if ($node['level'] < $nodes[$currentOpenNode]['level']) { 715 $currentOpenNode = $i; 716 } else { 717 $openNodes .= "$i "; 718 } 719 } 720 //insert node in last position 721 $q[] = $i; 722 } 723 $out .= $jscmd . "('" . idfilter($node['id'], false) . "',$i," . $father 724 . "," . json_encode($node['title']); 725 //hns 726 if ($node['hns']) { 727 $out .= ",'" . idfilter($node['hns'], false) . "'"; 728 } else { 729 $out .= ",0"; 730 } 731 if ($node['type'] == 'd' || $node['type'] == 'l') { 732 $out .= ",1"; 733 } else { 734 $out .= ",0"; 735 } 736 //MAX option 737 if ($node['type'] == 'l') { 738 $out .= ",1"; 739 } else { 740 $out .= ",0"; 741 } 742 $out .= ")" . $separator; 743 } 744 $openNodes = rtrim($openNodes, ' '); 745 return [$out, $openNodes]; 746 } 747 748 /** 749 * Parse namespace request 750 * 751 * @param string $ns namespaceid 752 * @param bool $id page id to resolve $ns relative to. 753 * @return string id of namespace 754 * 755 * @author Samuele Tognini <samuele@samuele.netsons.org> 756 */ 757 public function parseNs($ns, $id = false) 758 { 759 if ($id === false) { 760 global $ID; 761 $id = $ID; 762 } 763 //Just for old releases compatibility, .. was an old version for : in the docs of indexmenu 764 if ($ns == '..') { 765 $ns = ":"; 766 } 767 $ns = "$ns:arandompagehere"; 768 $resolver = new PageResolver($id); 769 $ns = getNs($resolver->resolveId($ns)); 770 return $ns === false ? '' : $ns; 771 } 772 773 /** 774 * Clean index data from unwanted nodes in nojs mode. 775 * 776 * @param array $nodes nodes of the tree 777 * @return void 778 * 779 * @author Samuele Tognini <samuele@samuele.netsons.org> 780 */ 781 private function cleanNojsData(&$nodes) 782 { 783 $a = 0; 784 foreach ($nodes as $i => $node) { 785 //all entries before $a are unset 786 if ($i < $a) { 787 continue; 788 } 789 //closed node 790 if ($node['type'] == "d" && !$node['open']) { 791 $a = $i + 1; 792 $level = $node['level']; 793 //search and remove every lower and closed nodes 794 while (isset($nodes[$a]) && $nodes[$a]['level'] > $level && !$nodes[$a]['open']) { 795 unset($nodes[$a]); 796 $a++; 797 } 798 } 799 } 800 } 801 802 803 /** 804 * Callback to print a Indexmenu item 805 * 806 * User function for @param array $item item described by array with at least the entries 807 * - id page id/namespace id 808 * - type 'd', 'l'(directory which is not yet opened) or 'f' 809 * - open is node open 810 * - title title of link 811 * - hns page id of headpage of the namespace or false 812 * @return string html of the content of a list item 813 * 814 * @author Samuele Tognini <samuele@samuele.netsons.org> 815 * @author Rik Blok 816 * @author Andreas Gohr <andi@splitbrain.org> 817 * 818 * @see html_buildlist() 819 */ 820 public function formatIndexmenuItem($item) 821 { 822 global $INFO; 823 $ret = ''; 824 825 //namespace 826 if ($item['type'] == 'd' || $item['type'] == 'l') { 827 $markCurrentPage = false; 828 829 $link = $item['id']; 830 $more = 'idx=' . $item['id']; 831 //namespace link 832 if ($item['hns']) { 833 $link = $item['hns']; 834 $tagid = "indexmenu_idx_head"; 835 $more = ''; 836 //current page is shown? 837 $markCurrentPage = $this->getConf('hide_headpage') && $item['hns'] == $INFO['id']; 838 } else { 839 //namespace without headpage 840 $tagid = "indexmenu_idx"; 841 if ($item['open']) { 842 $tagid .= ' open'; 843 } 844 } 845 846 if ($markCurrentPage) { 847 $ret .= '<span class="curid">'; 848 } 849 $ret .= '<a href="' . wl($link, $more) . '" class="' . $tagid . '">' 850 . $item['title'] 851 . '</a>'; 852 if ($markCurrentPage) { 853 $ret .= '</span>'; 854 } 855 return $ret; 856 } else { 857 //page link 858 return html_wikilink(':' . $item['id']); 859 } 860 } 861} 862