1<?php
2/**
3 * AcMenu plugin: an accordion menu for namespaces and relative pages.
4 *
5 * syntax.php: methods used by AcMenu plugin which extends DokuWiki's syntax.
6 *
7 * @author Torpedo <dcstoyanov@gmail.com>
8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html)
9 * @package syntax
10 */
11
12 if (!defined("DOKU_INC")) die();  // the plugin must be run within Dokuwiki
13
14 /**
15 * Define the methods used by AcMenu plugin to produce the plugin's output.
16 *
17 * @package syntax_acmenu
18 */
19class syntax_plugin_acmenu extends DokuWiki_Syntax_Plugin
20{
21
22    /**
23     * Define the syntax types that this plugin applies when founds its token.
24     *
25     * @return string
26     */
27    public function getType()
28    {
29        return "substition";
30    }
31
32    /**
33     * Define how the plugin's output is handled regarding paragraphs.
34     *
35     * Open paragraphs will be closed before plugin's output:
36     * <p>foo</p>
37     * <acmenu>
38     * <p>bar</p>
39     *
40     * @return string
41     */
42    public function getPType()
43    {
44        return "block";
45    }
46
47    /**
48     * Define the priority used to determine in which order modes are
49     * added: the mode with the lowest sort number will win.
50     *
51     * Since this plugin provides internal links, it is sorted at:
52     * AcMenu plugin < Doku_Parser_Mode_internallink (= 300)
53     *
54     * @return integer
55     */
56    public function getSort()
57    {
58        return 295;
59    }
60
61    /**
62     * Define the regular expression needed to match the plugin's syntax.
63     *
64     * This plugin use the following general syntax:
65     * <acmenu [list of parameters]>
66     *
67     * @param string $mode
68     *      name for the format mode of the final output
69     */
70    public function connectTo($mode)
71    {
72        $this->Lexer->addSpecialPattern("<acmenu.*?>", $mode, "plugin_acmenu");
73    }
74
75    /**
76     * Handle the plugin's syntax matched.
77     *
78     * This function is called every time the parser encounters the
79     * plugin's syntax in order to produce a list of instructions for the
80     * renderer, which will be interpreted later.
81     *
82     * @param string $match
83     *      the text matched
84     * @param integer $state
85     *      the lexer state
86     * @param integer $pos
87     *      the character position of the matched text
88     * @param object $handler
89     *      object reference to the Doku_Handler class
90     * @return array $data
91     *      the parameters handled by the plugin's syntax
92     */
93    public function handle($match, $state, $pos, Doku_Handler $handler)
94    {
95        $data = array();
96        $match = substr($match, 7, -1);  // strips "<acmenu" and ">"
97
98        return $data;
99    }
100
101    /**
102     * Process the list of instructions that render the final output.
103     *
104     * @param string $mode
105     *      name for the format mode of the final output
106     * @param object $renderer
107     *      object reference to the Doku_Render class
108     * @param array $data
109     *      the parameters handled by the plugin's syntax
110     */
111    public function render($mode, Doku_Renderer $renderer, $data)
112    {
113        global $INFO;
114        global $conf;
115
116        // disable the cache
117        $renderer->nocache();
118
119        // get the namespace genealogy of the current id
120        // and store it as metadata for been used in javascript
121        $sub_ns = $this->_get_sub_ns($INFO["id"]);
122        p_set_metadata($INFO["id"], array("plugin" => array("plugin_acmenu" => array("sub_ns" => $sub_ns))), false, false);
123
124        // build the namespace tree structure
125        $ns_acmenu = $this->_get_ns_acmenu($sub_ns);  // namespace in which <acmenu> is
126        $level = 0;
127        $tree = $this->_tree($ns_acmenu, $level);
128        $tree = $this->_sort_ns_pg($tree);
129
130        // get heading and id of the namespace in which <acmenu> is
131        $base_id = implode(":", array_filter(array($ns_acmenu, $conf["start"])));
132        $base_heading = p_get_first_heading($base_id);
133        if (!isset($base_heading)) {
134            $base_heading = $ns_acmenu;
135        }
136
137        // get cookies
138        $open_items = $this->_get_cookie();
139
140        // print the namespace tree structure
141        $renderer->doc .= "<div class='acmenu'>";
142        $renderer->doc .= "<ul class='idx'>";
143        $renderer->doc .= "<li class='open'>";
144        $renderer->doc .= "<div class='li'>";
145        $renderer->doc .= "<span class='curid'>";
146        $renderer->internallink($base_id, $base_heading);
147        $renderer->doc .= "</span>";
148        $renderer->doc .= "</div>";
149        $renderer->doc .= "<ul class='idx'>";
150        $this->_print($renderer, $tree, $sub_ns, $open_items);
151        $renderer->doc .= "</ul>";
152        $renderer->doc .= "</li>";
153        $renderer->doc .= "</ul>";
154        $renderer->doc .= "</div>";
155        // only if javascript is enabled and only at the first time
156        // hide the content of each namespace
157        // except those that are parents of the page selected
158        if (!isset($open_items) || isset($open_items) && count($open_items) == 0) {
159            $renderer->doc .= "<script type='text/javascript'>";
160            $renderer->doc .= "jQuery('div.acmenu ul.idx li.open div.li:not(:has(span.curid))')
161                               .next().hide()
162                               .parent().removeClass('open').addClass('closed');";
163            $renderer->doc .= "</script>";
164        }
165    }
166
167    /**
168     * Get cookies.
169     *
170     * @return array $open_items
171     *      the id of the <start> pages to keep open in the form:
172     *      array {
173     *      [i] => (str) "<ns-acmenu>:<ns-1>:..:<ns-i>:<start>"
174     *      }
175     */
176    private function _get_cookie()
177    {
178        $open_items = $_COOKIE["plugin_acmenu_open_items"];
179        if (isset($open_items)) {
180            $open_items = json_decode($open_items);
181        } else {
182            $open_items = array();
183        }
184
185        return $open_items;
186    }
187
188    /**
189     * Get the namespace's name in which <acmenu> is.
190     *
191     * Start from the current namespace (the namespace of the current page)
192     * and go up till the base namespace (the namespace in which <acmenu> is).
193     *
194     * @param array $sub_ns
195     *      the namespace genealogy of the current page's id, in the form:
196     *      array {
197     *      [0] => (str) "<ns-acmenu>:<ns-1>:...:<ns-i>"
198     *      ...
199     *      [i-1] => (str) "<ns-acmenu>"
200     *      [i] => (str) ""
201     *      }
202     * @return string $ns_acmenu
203     *      the namespace's name in which <acmenu> is, in the form:
204     *      <ns-acmenu>
205     */
206    private function _get_ns_acmenu($sub_ns)
207    {
208        global $INFO;
209        global $conf;
210        $dir = realpath($conf["savedir"] . "/pages");
211        $ns_acmenu = "";
212
213        if (!empty($INFO["namespace"])) {
214            foreach ($sub_ns as $ns) {
215                $sidebar = implode("/", array_filter(array(str_replace(":", "/", $ns), $conf["sidebar"])));
216                if (file_exists($dir . "/" . $sidebar . ".txt")) {
217                    $ns_acmenu = $ns;
218                    break;
219                }
220            }
221        }
222
223        return $ns_acmenu;
224    }
225
226    /**
227     * Build the namespace tree structure.
228     *
229     * Start from the base namespace (the namespace in which <acmenu> is)
230     * and go down until the end.
231     * If the "startop" option is enabled, the start page isn't renamed.
232     *
233     * @param string $ns_acmenu
234     *      the namespace's name in which <acmenu> is, in the form:
235     *      <ns-acmenu>
236     * @param string $level
237     *      the indentation level from which start to build the tree structure
238     * @return array $tree
239     *      the namespace tree, in the form:
240     *      array {
241     *      [0] => array {
242     *          ["heading"] => (str) "<heading>"
243     *          ["id"] => (str) "<id>"
244     *          ["level"] => (int) "<level>"
245     *          ["type"] => (str) "ns"
246     *          ["sub"] => array {
247     *              [0] => array {
248     *                  ["heading"] => (str) "<heading>"
249     *                  ["id"] => (str) "<id>"
250     *                  ["level"] => (int) "<level>"
251     *                  ["type"] => (str) "pg"
252     *                  }
253     *              [i] => array {...}
254     *              }
255     *          }
256     *      [i] => array {...}
257     *      }
258     *      where:
259     *      ["type"] = "ns" means "namespace"
260     *      ["type"] = "pg" means "page"
261     *      so that only namespaces can have ["sub"] namespaces
262     */
263    private function _tree($ns_acmenu, $level)
264    {
265        global $INFO;
266        global $conf;
267        $tree = array();
268        $level = $level + 1;
269        $dir = $conf["savedir"] ."/pages/" . str_replace(":", "/", $ns_acmenu);
270        $files = array_diff(scandir($dir), array("..", "."));
271        foreach ($files as $file) {
272            if (is_file($dir . "/" . $file)) {
273                $pg_name = cleanID(utf8_decodeFN(substr_replace($file, "", -strlen(".txt"))));
274                $id = implode(":", array_filter(array($ns_acmenu, $pg_name), "strlen"));
275                if (!isHiddenPage($id)) {
276                    if (auth_quickaclcheck($id) >= AUTH_READ) {
277                        $heading = $pg_name;
278                        if (useheading("navigation") && $pg_name != $conf["start"]) {
279                                $heading = p_get_first_heading($id);
280                        }
281                        $tree[] = array("heading" => $heading,
282                                        "id" => $id,
283                                        "level" => $level,
284                                        "type" => "pg");
285                    }
286                }
287            } else {
288                $id = implode(":", array_filter(array($ns_acmenu, $file, $conf["start"]), "strlen"));
289                if ($conf["sneaky_index"] && auth_quickaclcheck($id) < AUTH_READ) {
290                    continue;
291                } else {
292                    $heading = $file;
293                    if (useheading("navigation")) {
294                        $heading = p_get_first_heading($id);
295                    }
296                    if (file_exists($dir . "/" . $file . "/" . $conf["sidebar"] . ".txt")) {
297                        // subnamespace with a sidebar will not be scanned
298                        $tree[] = array("heading" => $heading,
299                                        "id" => $id,
300                                        "level" => $level,
301                                        "type" => "pg");
302                        continue;
303                    } else {
304                        $tree[] = array("heading" => $heading,
305                                        "id" => $id,
306                                        "level" => $level,
307                                        "type" => "ns",
308                                        "sub" => $this->_tree(implode(":", array_filter(array($ns_acmenu, $file), "strlen")), $level));
309                    }
310                }
311            }
312        }
313
314        return $tree;
315    }
316
317    /**
318     * Get the namespace genealogy of the given id.
319     *
320     * @param string $id
321     *      the current page's id, in the form:
322     *      <ns-acmenu>:<ns-1>:...:<ns-i>:<pg>
323     * @return array $sub_ns
324     *      the namespace genealogy of the current page's id, in the form:
325     *      array {
326     *      [0] => (str) "<ns-acmenu>:<ns-1>:...:<ns-i>"
327     *      ...
328     *      [i-1] => (str) "<ns-acmenu>"
329     *      [i] => (str) ""
330     *      }
331     */
332    private function _get_sub_ns($id)
333    {
334        $sub_ns = array();
335        $pieces = explode(":", $id);
336        array_pop($pieces);  // remove <pg>
337
338        $cp_pieces = $pieces;
339        foreach ($pieces as $key => $val) {
340            $sub_ns[] = implode(":", $cp_pieces);
341            array_pop($cp_pieces);
342        }
343        $sub_ns[] = "";
344
345        return $sub_ns;
346    }
347
348    /**
349     * Print the namespace tree structure.
350     *
351     * @param object $renderer
352     *      object reference to the Doku_Render class
353     * @param array $tree
354     *      the namespace tree, in the form:
355     *      array {
356     *      [0] => array {
357     *          ["heading"] => (str) "<heading>"
358     *          ["id"] => (str) "<id>"
359     *          ["level"] => (int) "<level>"
360     *          ["type"] => (str) "ns"
361     *          ["sub"] => array {
362     *              [0] => array {
363     *                  ["heading"] => (str) "<heading>"
364     *                  ["id"] => (str) "<id>"
365     *                  ["level"] => (int) "<level>"
366     *                  ["type"] => (str) "pg"
367     *                  }
368     *              [i] => array {...}
369     *              }
370     *          }
371     *      [i] => array {...}
372     *      }
373     *      where:
374     *      ["type"] = "ns" means "namespace"
375     *      ["type"] = "pg" means "page"
376     *      so that only namespaces can have ["sub"] namespaces
377     * @param array $sub_ns
378     *      the namespace genealogy of the current page's id, in the form:
379     *      array {
380     *      [0] => (str) "<ns-acmenu>:<ns-1>:...:<ns-i>"
381     *      ...
382     *      [i-1] => (str) "<ns-acmenu>"
383     *      [i] => (str) ""
384     *      }
385     * @param array $open_items
386     *      the namespaces to keep open, in the form:
387     *      array {
388     *      [i] => (str) "<ns_acmenu>:<ns-1>:...:<ns-i>"
389     *      }
390     */
391    private function _print($renderer, $tree, $sub_ns, $open_items)
392    {
393        global $INFO;
394        global $conf;
395        foreach ($tree as $key => $val) {
396            if ($val["type"] == "pg") {
397                $renderer->doc .= "<li class='level" . $val["level"]."'>";
398                $renderer->doc .= "<div class='li'>";
399                $renderer->internallink($val["id"], $val["heading"]);
400                $renderer->doc .= "</div>";
401                $renderer->doc .= "</li>";
402            } elseif ($val["type"] == "ns") {
403                if (in_array(substr($val["id"], 0, -strlen(":" . $conf["start"])), $sub_ns)
404                    || in_array($val["id"], $open_items)) {
405                    $renderer->doc .= "<li class='open'>";
406                } else {
407                    $renderer->doc .= "<li class='closed'>";
408                }
409                $renderer->doc .= "<div class='li'>";
410                if (in_array(substr($val["id"], 0, -strlen(":" . $conf["start"])), $sub_ns)) {
411                    $renderer->doc .= "<span class='curid'>";
412                    $renderer->internallink($val["id"], $val["heading"]);
413                    $renderer->doc .= "</span>";
414                } else {
415                    $renderer->internallink($val["id"], $val["heading"]);
416                }
417                $renderer->doc .= "</div>";
418                if (in_array(substr($val["id"], 0, -strlen(":" . $conf["start"])), $sub_ns)
419                    || in_array($val["id"], $open_items)) {
420                    $renderer->doc .= "<ul class='idx'>";
421                } else {
422                    $renderer->doc .= "<ul class='idx' style='display: none'>";
423                }
424                $this->_print($renderer, $val["sub"], $sub_ns, $open_items);
425                $renderer->doc .= "</ul>";
426                $renderer->doc .= "</li>";
427            }
428        }
429    }
430
431    /**
432     * Sort the tree namespace in ascending order.
433     *
434     * The tree is sorted in this order:
435     * 1) namespaces;
436     * 2) pages.
437     * If the "startop" option is enabled, the start page is kept on top.
438     * @param array $tree
439     *      the namespace tree, in the form:
440     *      array {
441     *      [0] => array {
442     *          ["heading"] => (str) "<heading>"
443     *          ["id"] => (str) "<id>"
444     *          ["level"] => (int) "<level>"
445     *          ["type"] => (str) "ns"
446     *          ["sub"] => array {
447     *              [0] => array {
448     *                  ["heading"] => (str) "<heading>"
449     *                  ["id"] => (str) "<id>"
450     *                  ["level"] => (int) "<level>"
451     *                  ["type"] => (str) "pg"
452     *                  }
453     *              [i] => array {...}
454     *              }
455     *          }
456     *      [i] => array {...}
457     *      }
458     *      where:
459     *      ["type"] = "ns" means "namespace"
460     *      ["type"] = "pg" means "page"
461     *      so that only namespaces can have ["sub"] namespaces
462     * @return array $tree
463     *      the tree namespace sorted
464     */
465    private function _sort_ns_pg($tree)
466    {
467        global $conf;
468        $ns = array();
469        $pg = array();
470
471        foreach ($tree as $key => $val) {
472            if ($val["type"] == "ns") {
473                $val["sub"] = $this->_sort_ns_pg($val["sub"]);
474                $ns[] = $val;
475            } else {
476                $pg[] = $val;
477            }
478        }
479        sort($ns);
480        sort($pg);
481        $tree = array_merge($ns, $pg);
482        foreach ($tree as $key => $array_val) {
483            if ($array_val["heading"] == $conf["start"]) {
484                unset($tree[$key]);
485                array_unshift($tree, $array_val);
486            }
487        }
488
489        return $tree;
490    }
491}
492