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