You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

867 lines
32 KiB

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