1<?php
2/**
3 * TocTweak plugin for DokuWiki; helper component
4 *
5 * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6 * @author     Satoshi Sahara <sahara.satoshi@gmail.com>
7 */
8if(!defined('DOKU_INC')) die();
9
10class helper_plugin_toctweak extends DokuWiki_Plugin {
11
12    /**
13     * overwrite Hash data defined in plugin.info.txt
14     */
15    function getPluginInfo(array $arrHash) {
16        $pluginInfoTxt = DOKU_PLUGIN.$this->getPluginName().'/plugin.info.txt';
17        return array_merge(confToHash($pluginInfoTxt), $arrHash);
18    }
19
20    /**
21     * syntax parser
22     */
23    function parse($param) {
24
25        // Ex: {{METATOC 2-4 width18 toc_hierarchical >id#section | title}}
26
27        // get tocTitle
28        if (strpos($param, '|') !== false) {
29            list($param, $tocTitle) = explode('|', $param);
30            // empty tocTitle will remove h3 'Table of Contents' headline
31            $tocTitle = trim($tocTitle);
32        } else {
33            $tocTitle = null;
34        }
35
36        // get id#section
37        list($param, $id) = explode('>', $param, 2);
38        list($id, $hash) = array_map('trim', explode('#', $id, 2));
39        $id = cleanID($id).($hash ? '#'.$hash : '');
40
41        // get other parameters
42        $params = explode(' ', $param);
43        foreach ($params as $token) {
44            if (empty($token)) continue;
45
46            // get TOC generation parameters, like "toptocleevl"-"maxtoclevel"
47            if (preg_match('/^(?:(\d+)-(\d+)|^(\-?\d+))$/', $token, $matches)) {
48                if (count($matches) == 4) {
49                    if (strpos($matches[3], '-') !== false) {
50                        $maxLv = abs($matches[3]);
51                    } else {
52                        $topLv = $matches[3];
53                    }
54                } else {
55                        $topLv = $matches[1];
56                        $maxLv = $matches[2];
57                }
58
59                if (isset($topLv)) {
60                    $topLv = ($topLv < 1) ? 1 : $topLv;
61                    $topLv = ($topLv > 5) ? 5 : $topLv;
62                } else {
63                    $topLv = $this->getConf('toptoclevel');
64                }
65
66                if (isset($maxLv)) {
67                    $maxLv = ($maxLv > 5) ? 5 : $maxLv;
68                } else {
69                    $maxLv = $this->getConf('maxtoclevel');
70                }
71                continue;
72            }
73
74            // get class name for TOC box, ensure excluded any malcious character
75            if (!preg_match('/[^ A-Za-z0-9_-]/', $token)) {
76                $classes[] = $token;
77            }
78        }
79        if (!empty($classes)) {
80            $tocClass = implode(' ', $classes);
81        } else {
82            $tocClass = null;
83        }
84
85        return array($topLv, $maxLv, $tocClass, $tocTitle, $id);
86    }
87
88    /**
89     * Get customized toc array using metadata of the page
90     */
91    function get_metatoc($id, $topLv=null, $maxLv=null, $headline='') {
92        global $ID, $INFO;
93
94        // retrieve TableOfContents from metadata
95        if ($id == $INFO['id']) {
96            $toc = $INFO['meta']['description']['tableofcontents'];
97        } else {
98            $toc = p_get_metadata($id,'description tableofcontents');
99        }
100        if ($toc == null) return array();
101
102        // get interested headline items
103        $toc = $this->_toc($toc, $topLv, $maxLv, $headline);
104
105        // modify toc array items directly within loop by reference
106        foreach ($toc as &$item) {
107            // add properties for toc of that is not current page
108            if ($id != $ID) {
109                // headlines should be found in other wiki page
110                $item['page']  = $id;
111                $item['url']   = wl($id).'#'.$item['hid'];
112                $item['class'] = 'wikilink1';
113            } else {
114                // headlines in current page (internal link)
115                $item['url']  = '#'.$item['hid'];
116            }
117        } // end of foreach
118        unset($item); // break the reference with the last item
119        return $toc;
120    }
121
122    /**
123     * toc array filter
124     */
125    function _toc(array $toc, $topLv=null, $maxLv=null, $headline='') {
126        global $conf;
127        $topLv = isset($topLv) ? $topLv : $this->getConf('toptoclevel');
128        $maxLv = isset($maxLv) ? $maxLv : $this->getConf('maxtoclevel');
129
130        $headline_matched = empty($headline);
131        $headline_level   = null;
132        $items = array();
133
134        foreach ($toc as $item) {
135            // skip non-interested toc entries
136            if ($headline) {
137                if (!$headline_matched) {
138                    if ($item['hid'] == $headline) {
139                        $headline_matched = true;
140                        $headline_level = $item['level'];
141                    } else {
142                        continue;
143                    }
144                } else {
145                    if ($item['level'] <= $headline_level) {
146                        $headline_matched = false;
147                        $headline_level = null;
148                        continue;
149                    }
150                }
151            }
152
153            // get headline level in real page
154            $Lv = $item['level'] + $conf['toptoclevel'] -1;
155
156            // exclude out-of-range item based on headline level
157            if (($Lv < $topLv)||($Lv > $maxLv)) {
158                continue;
159            } else { // interested toc entry
160                $item['level'] = $Lv - $topLv +1;
161            }
162            $items[] = $item;
163        }
164        return $items;
165    }
166
167    /**
168     * convert auto-toc array to XHTML tailored with class attibute
169     */
170    function html_toc(array $toc, ...$params) {
171        global $INFO;
172        $meta =& $INFO['meta']['toc'];
173
174        if ($this->getConf('tocminheads') == 0) return '';
175
176        if (count($params)) {
177            // apply toc array filter
178            list($topLv, $maxLv, $headline) = $params;
179            $toc = $this->_toc($toc, $topLv, $maxLv, $headline);
180        }
181
182        if (count($toc) < $this->getConf('tocminheads')) {
183            $html = '';
184        } else {
185            $html = html_TOC($toc); // use function in inc/html.php
186            if ($html && isset($meta['class'])) {
187                $search =  '<div id="dw__toc"';
188                $replace = $search.' class="'.hsc($meta['class']).'"';
189                $html = str_replace($search, $replace, $html);
190            }
191        }
192        return $html;
193    }
194
195}
196
197