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, fn($a, $b) => $this->itemComparator($a, $b, $useNatSort, $nsFirst)); 147 $levels[$level] = $items; 148 } 149 150 // merge levels into a flat list again 151 $levels = array_reverse($levels, true); 152 foreach (array_keys($levels) as $level) { 153 if ($level == $minlevel) break; 154 155 $parent = array_pop($parents); 156 $pos = array_search($parent, array_keys($levels[$level - 1])) + 1; 157 158 /** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */ 159 $levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) + 160 $levels[$level] + 161 array_slice($levels[$level - 1], $pos, null, true); 162 } 163 164 return $levels[$minlevel]; 165 } 166 167 /** 168 * Compare two items 169 * 170 * @param array $a 171 * @param array $b 172 * @param bool $useNatSort 173 * @param bool $nsFirst 174 * @return int 175 */ 176 public function itemComparator($a, $b, $useNatSort, $nsFirst) 177 { 178 if ($nsFirst && $a['type'] != $b['type']) { 179 return $a['type'] == 'd' ? -1 : 1; 180 } 181 182 if ($useNatSort) { 183 return Sort::strcmp($a['title'], $b['title']); 184 } else { 185 return strcmp($a['title'], $b['title']); 186 } 187 } 188 189 190 /** 191 * Create a list openening 192 * 193 * @param array $item 194 * @return string 195 * @see html_buildlist() 196 */ 197 public function cbList($item) 198 { 199 global $INFO; 200 201 if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) { 202 return '<strong>' . html_wikilink(':' . $item['id'], $item['title']) . '</strong>'; 203 } else { 204 return html_wikilink(':' . $item['id'], $item['title']); 205 } 206 } 207 208 /** 209 * Create a list item 210 * 211 * @param array $item 212 * @return string 213 * @see html_buildlist() 214 */ 215 public function cbListItem($item) 216 { 217 if ($item['type'] == "f") { 218 return '<li class="level' . $item['level'] . '">'; 219 } elseif ($item['open']) { 220 return '<li class="open">'; 221 } else { 222 return '<li class="closed">'; 223 } 224 } 225 226 /** 227 * Custom search callback 228 * 229 * @param $data 230 * @param $base 231 * @param $file 232 * @param $type 233 * @param $lvl 234 * @param array $opts - currentID is the currently shown page 235 * @return bool 236 */ 237 public function cbSearch(&$data, $base, $file, $type, $lvl, $opts) 238 { 239 global $conf; 240 $return = true; 241 242 $id = pathID($file); 243 244 if ( 245 $type == 'd' && !( 246 preg_match('#^' . $id . '(:|$)#', $opts['currentID']) || 247 preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID'])) 248 249 ) 250 ) { 251 //add but don't recurse 252 $return = false; 253 } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) { 254 //don't add 255 return false; 256 } 257 258 // for sneaky index, check access to the namespace's start page 259 if ($type == 'd' && $conf['sneaky_index']) { 260 $sp = (new PageResolver(''))->resolveId($id . ':'); 261 if (auth_quickaclcheck($sp) < AUTH_READ) { 262 return false; 263 } 264 } 265 266 if ($type == 'd') { 267 // link directories to their start pages 268 $original = $id; 269 $id = "$id:"; 270 $id = (new PageResolver(''))->resolveId($id); 271 $this->startpages[$id] = 1; 272 273 // if the resolve id is in the same namespace as the original it's a start page named like the dir 274 if (getNS($original) === getNS($id)) { 275 $useNS = $original; 276 } 277 } elseif (!empty($this->startpages[$id])) { 278 // skip already shown start pages 279 return false; 280 } 281 282 //check hidden 283 if (isHiddenPage($id)) { 284 return false; 285 } 286 287 //check ACL 288 if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) { 289 return false; 290 } 291 292 $data[$id] = [ 293 'id' => $id, 294 'type' => $type, 295 'level' => $lvl, 296 'open' => $return, 297 'title' => $this->getTitle($id, $opts['usetitle']), 298 'ns' => $useNS ?? (string)getNS($id), 299 ]; 300 301 return $return; 302 } 303 304 /** 305 * @param string $id 306 * @param bool $useTitle 307 * @return array 308 */ 309 protected function getMainStartPage($ns, $useTitle) 310 { 311 $resolver = new PageResolver(''); 312 $id = $resolver->resolveId($ns . ':'); 313 314 $item = [ 315 'id' => $id, 316 'type' => 'd', 317 'level' => 0, 318 'open' => true, 319 'title' => $this->getTitle($id, $useTitle), 320 'ns' => $ns, 321 ]; 322 $this->startpages[$id] = 1; 323 return $item; 324 } 325 326 /** 327 * Get the title for the given page ID 328 * 329 * @param string $id 330 * @param bool $usetitle - use the first heading as title 331 * @return string 332 */ 333 protected function getTitle($id, $usetitle) 334 { 335 global $conf; 336 337 if ($usetitle) { 338 $p = p_get_first_heading($id); 339 if (!empty($p)) return $p; 340 } 341 342 $p = noNS($id); 343 if ($p == $conf['start'] || !$p) { 344 $p = noNS(getNS($id)); 345 if (!$p) { 346 return $conf['start']; 347 } 348 } 349 return $p; 350 } 351} 352