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