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