1<?php 2 3use dokuwiki\Extension\SyntaxPlugin; 4use dokuwiki\File\PageResolver; 5use dokuwiki\Utf8\Sort; 6 7/** 8 * DokuWiki Plugin simplenavi (Syntax Component) 9 * 10 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 11 * @author Andreas Gohr <gohr@cosmocode.de> 12 */ 13class syntax_plugin_simplenavi extends SyntaxPlugin 14{ 15 private $startpages = []; 16 17 /** @inheritdoc */ 18 public function getType() 19 { 20 return 'substition'; 21 } 22 23 /** @inheritdoc */ 24 public function getPType() 25 { 26 return 'block'; 27 } 28 29 /** @inheritdoc */ 30 public function getSort() 31 { 32 return 155; 33 } 34 35 /** @inheritdoc */ 36 public function connectTo($mode) 37 { 38 $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 39 } 40 41 /** @inheritdoc */ 42 public function handle($match, $state, $pos, Doku_Handler $handler) 43 { 44 return explode(' ', substr($match, 13, -2)); 45 } 46 47 /** @inheritdoc */ 48 public function render($format, Doku_Renderer $renderer, $data) 49 { 50 if ($format != 'xhtml') return false; 51 52 global $INFO; 53 $renderer->nocache(); 54 55 // first data is namespace, rest is options 56 $ns = array_shift($data); 57 if ($ns && $ns[0] === '.') { 58 // resolve relative to current page 59 $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 60 } else { 61 $ns = cleanID($ns); 62 } 63 64 $items = $this->getSortedItems( 65 $ns, 66 $INFO['id'], 67 $this->getConf('usetitle'), 68 $this->getConf('natsort'), 69 $this->getConf('nsfirst'), 70 in_array('home', $data) 71 ); 72 73 $class = 'plugin__simplenavi'; 74 if (in_array('filter', $data)) $class .= ' plugin__simplenavi_filter'; 75 76 $renderer->doc .= '<div class="' . $class . '">'; 77 $renderer->doc .= html_buildlist($items, 'idx', [$this, 'cbList'], [$this, 'cbListItem']); 78 $renderer->doc .= '</div>'; 79 80 return true; 81 } 82 83 /** 84 * Fetch the items to display 85 * 86 * This returns a flat list suitable for html_buildlist() 87 * 88 * @param string $ns the namespace to search in 89 * @param string $current the current page, the tree will be expanded to this 90 * @param bool $useTitle Sort by the title instead of the ID? 91 * @param bool $useNatSort Use natural sorting or just sort by ASCII? 92 * @param bool $nsFirst Sort namespaces before pages? 93 * @param bool $home Add namespace's start page as top level item? 94 * @return array 95 */ 96 public function getSortedItems($ns, $current, $useTitle, $useNatSort, $nsFirst, $home) 97 { 98 global $conf; 99 100 // convert to path 101 $nspath = utf8_encodeFN(str_replace(':', '/', $ns)); 102 103 // get the start page of the main namespace, this adds it to the list of seen pages in $this->startpages 104 // and will skip it by default in the search callback 105 $startPage = $this->getMainStartPage($ns, $useTitle); 106 107 $items = []; 108 if ($home) { 109 // when home is requested, add the start page as top level item 110 $items[$startPage['id']] = $startPage; 111 $minlevel = 0; 112 } else { 113 $minlevel = 1; 114 } 115 116 // execute search using our own callback 117 search( 118 $items, 119 $conf['datadir'], 120 [$this, 'cbSearch'], 121 [ 122 'currentID' => $current, 123 'usetitle' => $useTitle, 124 ], 125 $nspath, 126 1, 127 '' // no sorting, we do ourselves 128 ); 129 if (!$items) return []; 130 131 // split into separate levels 132 $parents = []; 133 $levels = []; 134 $curLevel = $minlevel; 135 foreach ($items as $idx => $item) { 136 if ($curLevel < $item['level']) { 137 // previous item was the parent 138 $parents[] = array_key_last($levels[$curLevel]); 139 } 140 $curLevel = $item['level']; 141 $levels[$item['level']][$idx] = $item; 142 } 143 144 // sort each level separately 145 foreach ($levels as $level => $items) { 146 uasort($items, function ($a, $b) use ($useNatSort, $nsFirst) { 147 return $this->itemComparator($a, $b, $useNatSort, $nsFirst); 148 }); 149 $levels[$level] = $items; 150 } 151 152 // merge levels into a flat list again 153 $levels = array_reverse($levels, true); 154 foreach (array_keys($levels) as $level) { 155 if ($level == $minlevel) break; 156 157 $parent = array_pop($parents); 158 $pos = array_search($parent, array_keys($levels[$level - 1])) + 1; 159 160 /** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */ 161 $levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) + 162 $levels[$level] + 163 array_slice($levels[$level - 1], $pos, null, true); 164 } 165 166 return $levels[$minlevel]; 167 } 168 169 /** 170 * Compare two items 171 * 172 * @param array $a 173 * @param array $b 174 * @param bool $useNatSort 175 * @param bool $nsFirst 176 * @return int 177 */ 178 public function itemComparator($a, $b, $useNatSort, $nsFirst) 179 { 180 if ($nsFirst && $a['type'] != $b['type']) { 181 return $a['type'] == 'd' ? -1 : 1; 182 } 183 184 if ($useNatSort) { 185 return Sort::strcmp($a['title'], $b['title']); 186 } else { 187 return strcmp($a['title'], $b['title']); 188 } 189 } 190 191 192 /** 193 * Create a list openening 194 * 195 * @param array $item 196 * @return string 197 * @see html_buildlist() 198 */ 199 public function cbList($item) 200 { 201 global $INFO; 202 203 if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) { 204 return '<strong>' . html_wikilink(':' . $item['id'], $item['title']) . '</strong>'; 205 } else { 206 return html_wikilink(':' . $item['id'], $item['title']); 207 } 208 } 209 210 /** 211 * Create a list item 212 * 213 * @param array $item 214 * @return string 215 * @see html_buildlist() 216 */ 217 public function cbListItem($item) 218 { 219 if ($item['type'] == "f") { 220 return '<li class="level' . $item['level'] . '">'; 221 } elseif ($item['open']) { 222 return '<li class="open">'; 223 } else { 224 return '<li class="closed">'; 225 } 226 } 227 228 /** 229 * Custom search callback 230 * 231 * @param $data 232 * @param $base 233 * @param $file 234 * @param $type 235 * @param $lvl 236 * @param array $opts - currentID is the currently shown page 237 * @return bool 238 */ 239 public function cbSearch(&$data, $base, $file, $type, $lvl, $opts) 240 { 241 global $conf; 242 $return = true; 243 244 $id = pathID($file); 245 246 if ( 247 $type == 'd' && 248 ( 249 !preg_match('#^' . $id . '(:|$)#', $opts['currentID']) && 250 !preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID'])) 251 ) 252 ) { 253 //add but don't recurse 254 $return = false; 255 } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) { 256 //don't add 257 return false; 258 } 259 260 // for sneaky index, check access to the namespace's start page 261 if ($type == 'd' && $conf['sneaky_index']) { 262 $sp = (new PageResolver(''))->resolveId($id . ':'); 263 if (auth_quickaclcheck($sp) < AUTH_READ) { 264 return false; 265 } 266 } 267 268 if ($type == 'd') { 269 // link directories to their start pages 270 $original = $id; 271 $id = "$id:"; 272 $id = (new PageResolver(''))->resolveId($id); 273 $this->startpages[$id] = 1; 274 275 // if the resolve id is in the same namespace as the original it's a start page named like the dir 276 if (getNS($original) === getNS($id)) { 277 $useNS = $original; 278 } 279 } elseif (!empty($this->startpages[$id])) { 280 // skip already shown start pages 281 return false; 282 } 283 284 //check hidden 285 if (isHiddenPage($id)) { 286 return false; 287 } 288 289 //check ACL 290 if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) { 291 return false; 292 } 293 294 $data[$id] = [ 295 'id' => $id, 296 'type' => $type, 297 'level' => $lvl, 298 'open' => $return, 299 'title' => $this->getTitle($id, $opts['usetitle']), 300 'ns' => $useNS ?? (string)getNS($id), 301 ]; 302 303 return $return; 304 } 305 306 /** 307 * @param string $id 308 * @param bool $useTitle 309 * @return array 310 */ 311 protected function getMainStartPage($ns, $useTitle) 312 { 313 $resolver = new PageResolver(''); 314 $id = $resolver->resolveId($ns . ':'); 315 316 $item = [ 317 'id' => $id, 318 'type' => 'd', 319 'level' => 0, 320 'open' => true, 321 'title' => $this->getTitle($id, $useTitle), 322 'ns' => $ns, 323 ]; 324 $this->startpages[$id] = 1; 325 return $item; 326 } 327 328 /** 329 * Get the title for the given page ID 330 * 331 * @param string $id 332 * @param bool $usetitle - use the first heading as title 333 * @return string 334 */ 335 protected function getTitle($id, $usetitle) 336 { 337 global $conf; 338 339 if ($usetitle) { 340 $p = p_get_first_heading($id); 341 if (!empty($p)) return $p; 342 } 343 344 $p = noNS($id); 345 if ($p == $conf['start'] || !$p) { 346 $p = noNS(getNS($id)); 347 if (!$p) { 348 return $conf['start']; 349 } 350 } 351 return $p; 352 } 353} 354