1<?php 2/** 3 * Build a navigation menu from a list 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Andreas Gohr <gohr@cosmocode.de> 7 */ 8class syntax_plugin_navi extends DokuWiki_Syntax_Plugin 9{ 10 protected $defaultOptions = [ 11 'ns' => false, 12 'full' => false, 13 'js' => false, 14 ]; 15 16 /** * @inheritDoc */ 17 public function getType() 18 { 19 return 'substition'; 20 } 21 22 /** * @inheritDoc */ 23 public function getPType() 24 { 25 return 'block'; 26 } 27 28 /** * @inheritDoc */ 29 public function getSort() 30 { 31 return 155; 32 } 33 34 /** * @inheritDoc */ 35 public function connectTo($mode) 36 { 37 $this->Lexer->addSpecialPattern('{{navi>[^}]+}}', $mode, 'plugin_navi'); 38 } 39 40 /** * @inheritDoc */ 41 public function handle($match, $state, $pos, Doku_Handler $handler) 42 { 43 $id = substr($match, 7, -2); 44 $opts = ''; 45 if (strpos($id, '?') !== false) { 46 list($id, $opts) = explode('?', $id, 2); 47 } 48 $options = $this->parseOptions($opts); 49 $list = $this->parseNavigationControlPage(cleanID($id)); 50 51 return [wikiFN($id), $list, $options]; 52 } 53 54 /** 55 * @inheritDoc 56 * We handle all modes (except meta) because we pass all output creation back to the parent 57 */ 58 public function render($format, Doku_Renderer $R, $data) 59 { 60 $fn = $data[0]; 61 $navItems = $data[1]; 62 $options = $data[2]; 63 64 if ($format == 'metadata') { 65 $R->meta['relation']['naviplugin'][] = $fn; 66 return true; 67 } 68 69 $R->info['cache'] = false; // no cache please 70 71 $parentPath = $this->getOpenPath($navItems, $options); 72 73 $R->doc .= '<div class="plugin__navi ' . ($options['js'] ? 'js' : '') . '">'; 74 $this->renderTree($navItems, $parentPath, $R, $options['full']); 75 $R->doc .= '</div>'; 76 77 return true; 78 } 79 80 /** 81 * Simple accessor to call the plugin from templates 82 * 83 * @param string $controlPage 84 * @param array $options 85 * @return string the HTML tree 86 */ 87 public function tpl($controlPage, $options = []) 88 { 89 // resolve relative to the controlpage because we have no sidebar context 90 global $ID; 91 $oldid = $ID; 92 $ID = $controlPage; 93 94 $options = array_merge($this->defaultOptions, $options); 95 $R = new \Doku_Renderer_xhtml(); 96 $this->render('xhtml', $R, [ 97 wikiFN($controlPage), 98 $this->parseNavigationControlPage($controlPage), 99 $options, 100 ]); 101 102 $ID = $oldid; 103 return $R->doc; 104 } 105 106 /** 107 * Parses the items from the control page 108 * 109 * @param string $controlPage ID of the control page 110 * @return array list of navigational items 111 */ 112 public function parseNavigationControlPage($controlPage) 113 { 114 global $ID; 115 116 // fetch the instructions of the control page 117 $instructions = p_cached_instructions(wikiFN($controlPage), false, $controlPage); 118 if (!$instructions) return []; 119 120 // prepare some vars 121 $max = count($instructions); 122 $pre = true; 123 $lvl = 0; 124 $parents = array(); 125 $page = ''; 126 $cnt = 0; 127 128 // build a lookup table 129 $list = []; 130 for ($i = 0; $i < $max; $i++) { 131 if ($instructions[$i][0] == 'listu_open') { 132 $pre = false; 133 $lvl++; 134 if ($page) array_push($parents, $page); 135 } elseif ($instructions[$i][0] == 'listu_close') { 136 $lvl--; 137 array_pop($parents); 138 } elseif ($pre || $lvl == 0) { 139 unset($instructions[$i]); 140 } elseif ($instructions[$i][0] == 'listitem_close') { 141 $cnt++; 142 } elseif ($instructions[$i][0] == 'internallink') { 143 $foo = true; 144 $page = $instructions[$i][1][0]; 145 resolve_pageid(getNS($ID), $page, $foo); // resolve relative to sidebar ID 146 $list[$page] = array( 147 'parents' => $parents, 148 'page' => $page, 149 'title' => $instructions[$i][1][1], 150 'lvl' => $lvl, 151 ); 152 } elseif ($instructions[$i][0] == 'externallink') { 153 $url = $instructions[$i][1][0]; 154 $list['_' . $page] = array( 155 'parents' => $parents, 156 'page' => $url, 157 'title' => $instructions[$i][1][1], 158 'lvl' => $lvl, 159 ); 160 } 161 } 162 return $list; 163 } 164 165 /** 166 * Create a "path" of items to be opened above the current page 167 * 168 * @param array $navItems list of navigation items 169 * @param array $options Configuration options 170 * @return array 171 */ 172 public function getOpenPath($navItems, $options) 173 { 174 global $INFO; 175 $openPath = array(); 176 if (isset($navItems[$INFO['id']])) { 177 $openPath = (array)$navItems[$INFO['id']]['parents']; // get the "path" of the page we're on currently 178 array_push($openPath, $INFO['id']); 179 } elseif ($options['ns']) { 180 $ns = $INFO['id']; 181 182 // traverse up for matching namespaces 183 if ($navItems) { 184 do { 185 $ns = getNS($ns); 186 $try = "$ns:"; 187 resolve_pageid('', $try, $foo); 188 if (isset($navItems[$try])) { 189 // got a start page 190 $openPath = (array)$navItems[$try]['parents']; 191 array_push($openPath, $try); 192 break; 193 } else { 194 // search for the first page matching the namespace 195 foreach ($navItems as $key => $junk) { 196 if (getNS($key) == $ns) { 197 $openPath = (array)$navItems[$key]['parents']; 198 array_push($openPath, $key); 199 break 2; 200 } 201 } 202 } 203 204 } while ($ns); 205 } 206 } 207 return $openPath; 208 } 209 210 /** 211 * create a correctly nested list (or so I hope) 212 * 213 * @param array $navItems list of navigational items 214 * @param array $parentPath path of parent items 215 * @param Doku_Renderer $R should closed subitems still be rendered? 216 * @param bool $fullTree 217 */ 218 public function renderTree($navItems, $parentPath, Doku_Renderer $R, $fullTree = false) 219 { 220 $open = false; 221 $lvl = 1; 222 $R->listu_open(); 223 224 // read if item has childs and if it is open or closed 225 $upper = array(); 226 foreach ((array)$navItems as $pid => $info) { 227 $state = (array_diff($info['parents'], $parentPath)) ? 'close' : ''; 228 $countparents = count($info['parents']); 229 if ($countparents > '0') { 230 for ($i = 0; $i < $countparents; $i++) { 231 $upperlevel = $countparents - 1; 232 $upper[$info['parents'][$upperlevel]] = ($state == 'close') ? 'close' : 'open'; 233 } 234 } 235 } 236 unset($pid); 237 238 foreach ((array)$navItems as $pid => $info) { 239 // only show if we are in the "path" 240 if (!$fullTree && array_diff($info['parents'], $parentPath)) { 241 continue; 242 } 243 244 if (!empty($upper[$pid])) { 245 $menuitem = ($upper[$pid] == 'open') ? 'open' : 'close'; 246 } else { 247 $menuitem = ''; 248 } 249 250 // skip every non readable page 251 if (auth_quickaclcheck(cleanID($info['page'])) < AUTH_READ) { 252 continue; 253 } 254 255 if ($info['lvl'] == $lvl) { 256 if ($open) { 257 $R->listitem_close(); 258 } 259 $R->listitem_open($lvl . ' ' . $menuitem); 260 $open = true; 261 } elseif ($lvl > $info['lvl']) { 262 for (; $lvl > $info['lvl']; --$lvl) { 263 $R->listitem_close(); 264 $R->listu_close(); 265 } 266 $R->listitem_close(); 267 $R->listitem_open($lvl . ' ' . $menuitem); 268 } elseif ($lvl < $info['lvl']) { 269 // more than one run is bad nesting! 270 for (; $lvl < $info['lvl']; ++$lvl) { 271 $R->listu_open(); 272 $R->listitem_open($lvl + 1 . ' ' . $menuitem); 273 $open = true; 274 } 275 } 276 277 $R->listcontent_open(); 278 if (substr($pid, 0, 1) != '_') { 279 $R->internallink(':' . $info['page'], $info['title']); 280 } else { 281 $R->externallink($info['page'], $info['title']); 282 } 283 284 $R->listcontent_close(); 285 } 286 while ($lvl > 0) { 287 $R->listitem_close(); 288 $R->listu_close(); 289 --$lvl; 290 } 291 } 292 293 /** 294 * Parse the option string into an array 295 * 296 * @param string $opts 297 * @return array 298 */ 299 protected function parseOptions($opts) 300 { 301 $options = $this->defaultOptions; 302 303 foreach (explode('&', $opts) as $opt) { 304 $options[$opt] = true; 305 } 306 307 if ($options['js']) $options['full'] = true; 308 309 return $options; 310 } 311} 312