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