1<?php
2/**
3 * DokuWiki Plugin docnav (Syntax Component)
4 *
5 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html
6 * @author  Gerrit Uitslag <klapinklapin@gmail.com>
7 */
8
9/**
10 * Syntax for including a table of content of bundle of pages linked by docnavigation
11 */
12class syntax_plugin_docnavigation_toc extends DokuWiki_Syntax_Plugin
13{
14
15    /**
16     * Syntax Type
17     *
18     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
19     *
20     * @return string
21     */
22    public function getType()
23    {
24        return 'substition';
25    }
26
27    /**
28     * Paragraph Type
29     *
30     * Defines how this syntax is handled regarding paragraphs. This is important
31     * for correct XHTML nesting. Should return one of the following:
32     *
33     * 'normal' - The plugin can be used inside paragraphs
34     * 'block'  - Open paragraphs need to be closed before plugin output
35     * 'stack'  - Special case. Plugin wraps other paragraphs.
36     *
37     * @return string
38     * @see Doku_Handler_Block
39     *
40     */
41    public function getPType()
42    {
43        return 'block';
44    }
45
46    /**
47     * Sort for applying this mode
48     *
49     * @return int
50     */
51    public function getSort()
52    {
53        return 150;
54    }
55
56    /**
57     * @param string $mode
58     */
59    public function connectTo($mode)
60    {
61        $this->Lexer->addSpecialPattern('<doctoc\b.*?>', $mode, 'plugin_docnavigation_toc');
62    }
63
64    /**
65     * Handler to prepare matched data for the rendering process
66     *
67     * @param string $match The text matched by the patterns
68     * @param int $state The lexer state for the match
69     * @param int $pos The character position of the matched text
70     * @param Doku_Handler $handler The Doku_Handler object
71     * @return  array Return an array with all data you want to use in render, false don't add an instruction
72     */
73    public function handle($match, $state, $pos, Doku_Handler $handler)
74    {
75        global $ID;
76
77        $optstrs = substr($match, 7, -1); // remove "<doctoc"  and ">"
78        $optstrs = explode(',', $optstrs);
79        $options = [
80            'start' => $ID,
81            'includeheadings' => false,
82            'numbers' => false,
83            'useheading' => useHeading('navigation'),
84            'hidepagelink' => false
85        ];
86        foreach ($optstrs as $optstr) {
87            list($key, $value) = array_pad(explode('=', $optstr, 2), 2, '');
88            $value = trim($value);
89
90            switch (trim($key)) {
91                case 'start':
92                    $options['start'] = $this->getFullPageid($value);
93                    break;
94                case 'includeheadings':
95                    [$start, $end] = array_pad(explode('-', $value, 2), 2, '');
96                    $start = (int)$start;
97                    $end = (int)$end;
98
99                    if ($start < 1) {
100                        $start = 2;
101                    }
102
103                    if ($end < 1) {
104                        $end = $start;
105                    }
106
107                    //order from low to high
108                    if ($start > $end) {
109                        $level = $end;
110                        $end = $start;
111                        $start = $level;
112                    }
113                    $options['includeheadings'] = [$start, $end];
114                    break;
115                case 'numbers':
116                    $options['numbers'] = !empty($value);
117                    break;
118                case 'useheading':
119                    $options['useheading'] = !empty($value);
120                    break;
121                case 'hidepagelink':
122                    $options['hidepagelink'] = !empty($value);
123                    break;
124            }
125        }
126        if ($options['hidepagelink'] && $options['includeheadings'] === false) {
127            $options['includeheadings'] = [1, 2];
128        }
129        return $options;
130    }
131
132    /**
133     * Handles the actual output creation.
134     *
135     * @param string $format output format being rendered
136     * @param Doku_Renderer $renderer the current renderer object
137     * @param array $options data created by handler()
138     * @return  boolean                 rendered correctly? (however, returned value is not used at the moment)
139     */
140    public function render($format, Doku_Renderer $renderer, $options)
141    {
142        global $ID;
143        global $ACT;
144
145        if ($format != 'xhtml') return false;
146        /** @var Doku_Renderer_xhtml $renderer */
147
148        $renderer->nocache();
149
150        $list = [];
151        $recursioncheck = []; //needed for 'hidepagelink' option
152        $pageid = $options['start'];
153        $previouspage = null;
154        while ($pageid !== null) {
155            $pageitem = [];
156            $pageitem['id'] = $pageid;
157            $pageitem['ns'] = getNS($pageitem['id']);
158            $pageitem['type'] = $options['includeheadings'] === false ? 'pageonly' : 'pagewithheadings'; //page or heading
159            $pageitem['level'] = 1;
160            $pageitem['ordered'] = $options['numbers'];
161
162            if ($options['useheading']) {
163                $pageitem['title'] = p_get_first_heading($pageitem['id'], METADATA_DONT_RENDER);
164            } else {
165                $pageitem['title'] = null;
166            }
167            $pageitem['perm'] = auth_quickaclcheck($pageitem['id']);
168
169            if ($pageitem['perm'] >= AUTH_READ) {
170
171                if ($options['hidepagelink']) {
172                    $tocitemlevel = 1;
173                    //recursive check needs a list of added pages
174                    $recursioncheck[$pageid] = true;
175                } else {
176                    //add page to list
177                    $list[$pageid] = $pageitem;
178                    $tocitemlevel = 2;
179                }
180
181                if (!empty($options['includeheadings'])) {
182                    $toc = p_get_metadata($pageid, 'description tableofcontents', METADATA_RENDER_USING_CACHE | METADATA_RENDER_UNLIMITED);
183
184                    $first = true;
185                    if (is_array($toc)) foreach ($toc as $tocitem) {
186                        if ($tocitem['level'] < $options['includeheadings'][0] || $tocitem['level'] > $options['includeheadings'][1]) {
187                            continue;
188                        }
189                        $item = [];
190                        $item['id'] = $pageid . '#' . $tocitem['hid'];
191                        $item['ns'] = getNS($item['id']);
192                        if ($options['hidepagelink'] && $first) {
193                            //mark only first heading(=title), if no pages are shown
194                            $item['type'] = 'firstheading';
195                            $first = false;
196                        } else {
197                            $item['type'] = 'heading';
198                        }
199
200                        $item['level'] = $tocitemlevel + $tocitem['level'] - $options['includeheadings'][0];
201                        $item['title'] = $tocitem['title'];
202
203                        $list[$item['id']] = $item;
204                    }
205                }
206            }
207
208            if ($ACT == 'preview' && $pageid === $ID) {
209                // the RENDERER_CONTENT_POSTPROCESS event is triggered just after rendering the instruction,
210                // so syntax instance will exists
211                /** @var syntax_plugin_docnavigation_pagenav $pagenav */
212                $pagenav = plugin_load('syntax', 'docnavigation_pagenav');
213                if ($pagenav) {
214                    $pagedata = $pagenav->data[$pageid];
215                } else {
216                    $pagedata = [];
217                }
218            } else {
219                $pagedata = p_get_metadata($pageid, 'docnavigation');
220            }
221
222            //check referer
223            if (empty($pagedata['previous']['link']) || $pagedata['previous']['link'] != $previouspage) {
224
225                // is not first page or non-existing page (so without syntax)?
226                if ($previouspage !== null && page_exists($pageid)) {
227                    msg(sprintf($this->getLang('dontlinkback'), $pageid, $previouspage), -1);
228                }
229            }
230
231            $previouspage = $pageid;
232            $nextpageid = $pagedata['next']['link'];
233            if (empty($nextpageid)) {
234                $pageid = null;
235            } elseif ($options['hidepagelink'] ? isset($recursioncheck[$nextpageid]) : isset($list[$nextpageid])) {
236                msg(sprintf($this->getLang('recursionprevented'), $pageid, $nextpageid), -1);
237                $pageid = null;
238            } else {
239                $pageid = $nextpageid;
240            }
241        }
242
243        $renderer->doc .= html_buildlist($list, 'pagnavtoc', [$this, 'listItemNavtoc']);
244
245        return true;
246    }
247
248    /**
249     * Index item formatter
250     *
251     * User function for html_buildlist()
252     *
253     * @param array $item
254     * @return string
255     * @author Andreas Gohr <andi@splitbrain.org>
256     *
257     */
258    public function listItemNavtoc($item)
259    {
260        // default is noNSorNS($id), but we want noNS($id) when useheading is off FS#2605
261        if ($item['title'] === null) {
262            $name = noNS($item['id']);
263        } else {
264            $name = $item['title'];
265        }
266
267        $ret = '';
268        $link = html_wikilink(':' . $item['id'], $name);
269        if ($item['type'] == 'pagewithheadings' || $item['type'] == 'firstheading') {
270            $ret .= '<strong>';
271            $ret .= $link;
272            $ret .= '</strong>';
273        } else {
274            $ret .= $link;
275        }
276        return $ret;
277    }
278
279    /**
280     * Callback for html_buildlist
281     *
282     * @param array $item
283     * @return string html
284     */
285    function html_list_toc($item)
286    {
287        if (isset($item['hid'])) {
288            $link = '#' . $item['hid'];
289        } else {
290            $link = $item['link'];
291        }
292
293        return '<a href="' . $link . '">' . hsc($item['title']) . '</a>';
294    }
295
296    /**
297     * Resolves given id against current page to full pageid, removes hash
298     *
299     * @param string $pageid
300     * @return mixed
301     */
302    public function getFullPageid($pageid)
303    {
304        global $ID;
305        // Igor and later
306        if (class_exists('dokuwiki\File\PageResolver')) {
307            $resolver = new dokuwiki\File\PageResolver($ID);
308            $pageid = $resolver->resolveId($pageid);
309        } else {
310            // Compatibility with older releases
311            resolve_pageid(getNS($ID), $pageid, $exists);
312        }
313        [$page, /* $hash */] = array_pad(explode('#', $pageid, 2), 2, '');
314        return $page;
315    }
316
317}
318