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