11169a1acSAndreas Gohr<?php 2d8ce5486SAndreas Gohr 3*d418c031SAndreas Gohruse dokuwiki\Extension\SyntaxPlugin; 4d8ce5486SAndreas Gohruse dokuwiki\File\PageResolver; 5e75a33bfSAndreas Gohruse dokuwiki\Utf8\Sort; 6d8ce5486SAndreas Gohr 71169a1acSAndreas Gohr/** 81169a1acSAndreas Gohr * DokuWiki Plugin simplenavi (Syntax Component) 91169a1acSAndreas Gohr * 101169a1acSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 111169a1acSAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 121169a1acSAndreas Gohr */ 13*d418c031SAndreas Gohrclass syntax_plugin_simplenavi extends SyntaxPlugin 14d8ce5486SAndreas Gohr{ 15d8ce5486SAndreas Gohr private $startpages = []; 161169a1acSAndreas Gohr 17d8ce5486SAndreas Gohr /** @inheritdoc */ 18d8ce5486SAndreas Gohr public function getType() 19d8ce5486SAndreas Gohr { 201169a1acSAndreas Gohr return 'substition'; 211169a1acSAndreas Gohr } 221169a1acSAndreas Gohr 23d8ce5486SAndreas Gohr /** @inheritdoc */ 24d8ce5486SAndreas Gohr public function getPType() 25d8ce5486SAndreas Gohr { 261169a1acSAndreas Gohr return 'block'; 271169a1acSAndreas Gohr } 281169a1acSAndreas Gohr 29d8ce5486SAndreas Gohr /** @inheritdoc */ 30d8ce5486SAndreas Gohr public function getSort() 31d8ce5486SAndreas Gohr { 321169a1acSAndreas Gohr return 155; 331169a1acSAndreas Gohr } 341169a1acSAndreas Gohr 35d8ce5486SAndreas Gohr /** @inheritdoc */ 36d8ce5486SAndreas Gohr public function connectTo($mode) 37d8ce5486SAndreas Gohr { 381169a1acSAndreas Gohr $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 391169a1acSAndreas Gohr } 401169a1acSAndreas Gohr 41d8ce5486SAndreas Gohr /** @inheritdoc */ 42d8ce5486SAndreas Gohr public function handle($match, $state, $pos, Doku_Handler $handler) 43d8ce5486SAndreas Gohr { 445655937aSAndreas Gohr return explode(' ', substr($match, 13, -2)); 451169a1acSAndreas Gohr } 461169a1acSAndreas Gohr 47d8ce5486SAndreas Gohr /** @inheritdoc */ 48d8ce5486SAndreas Gohr public function render($format, Doku_Renderer $renderer, $data) 49d8ce5486SAndreas Gohr { 50d8ce5486SAndreas Gohr if ($format != 'xhtml') return false; 511169a1acSAndreas Gohr 521169a1acSAndreas Gohr global $INFO; 53b3e02951SAndreas Gohr $renderer->nocache(); 541169a1acSAndreas Gohr 55b3e02951SAndreas Gohr // first data is namespace, rest is options 565655937aSAndreas Gohr $ns = array_shift($data); 575655937aSAndreas Gohr if ($ns && $ns[0] === '.') { 585655937aSAndreas Gohr // resolve relative to current page 595655937aSAndreas Gohr $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 605655937aSAndreas Gohr } else { 615655937aSAndreas Gohr $ns = cleanID($ns); 625655937aSAndreas Gohr } 63b3e02951SAndreas Gohr 64e75a33bfSAndreas Gohr $items = $this->getSortedItems( 65e75a33bfSAndreas Gohr $ns, 66e75a33bfSAndreas Gohr $INFO['id'], 67e75a33bfSAndreas Gohr $this->getConf('usetitle'), 68e58e2f72SAndreas Gohr $this->getConf('natsort'), 69*d418c031SAndreas Gohr $this->getConf('nsfirst'), 70*d418c031SAndreas Gohr in_array('home', $data) 71e75a33bfSAndreas Gohr ); 721169a1acSAndreas Gohr 73b3e02951SAndreas Gohr $class = 'plugin__simplenavi'; 74b3e02951SAndreas Gohr if (in_array('filter', $data)) $class .= ' plugin__simplenavi_filter'; 75b3e02951SAndreas Gohr 76b3e02951SAndreas Gohr $renderer->doc .= '<div class="' . $class . '">'; 77d8ce5486SAndreas Gohr $renderer->doc .= html_buildlist($items, 'idx', [$this, 'cbList'], [$this, 'cbListItem']); 78d8ce5486SAndreas Gohr $renderer->doc .= '</div>'; 791169a1acSAndreas Gohr 801169a1acSAndreas Gohr return true; 811169a1acSAndreas Gohr } 821169a1acSAndreas Gohr 83d8ce5486SAndreas Gohr /** 84e75a33bfSAndreas Gohr * Fetch the items to display 85e75a33bfSAndreas Gohr * 86e75a33bfSAndreas Gohr * This returns a flat list suitable for html_buildlist() 87e75a33bfSAndreas Gohr * 88e75a33bfSAndreas Gohr * @param string $ns the namespace to search in 89e75a33bfSAndreas Gohr * @param string $current the current page, the tree will be expanded to this 90e75a33bfSAndreas Gohr * @param bool $useTitle Sort by the title instead of the ID? 91e75a33bfSAndreas Gohr * @param bool $useNatSort Use natural sorting or just sort by ASCII? 92*d418c031SAndreas Gohr * @param bool $nsFirst Sort namespaces before pages? 93*d418c031SAndreas Gohr * @param bool $home Add namespace's start page as top level item? 94e75a33bfSAndreas Gohr * @return array 95e75a33bfSAndreas Gohr */ 96*d418c031SAndreas Gohr public function getSortedItems($ns, $current, $useTitle, $useNatSort, $nsFirst, $home) 97e75a33bfSAndreas Gohr { 98e75a33bfSAndreas Gohr global $conf; 99e75a33bfSAndreas Gohr 100*d418c031SAndreas Gohr // convert to path 101*d418c031SAndreas Gohr $nspath = utf8_encodeFN(str_replace(':', '/', $ns)); 102*d418c031SAndreas Gohr 103*d418c031SAndreas Gohr // get the start page of the main namespace, this adds it to the list of seen pages in $this->startpages 104*d418c031SAndreas Gohr // and will skip it by default in the search callback 105*d418c031SAndreas Gohr $startPage = $this->getMainStartPage($ns, $useTitle); 106*d418c031SAndreas Gohr 107e75a33bfSAndreas Gohr $items = []; 108*d418c031SAndreas Gohr if ($home) { 109*d418c031SAndreas Gohr // when home is requested, add the start page as top level item 110*d418c031SAndreas Gohr $items[$startPage['id']] = $startPage; 111*d418c031SAndreas Gohr $minlevel = 0; 112*d418c031SAndreas Gohr } else { 113*d418c031SAndreas Gohr $minlevel = 1; 114*d418c031SAndreas Gohr } 115*d418c031SAndreas Gohr 116*d418c031SAndreas Gohr // execute search using our own callback 117e75a33bfSAndreas Gohr search( 118e75a33bfSAndreas Gohr $items, 119e75a33bfSAndreas Gohr $conf['datadir'], 120e75a33bfSAndreas Gohr [$this, 'cbSearch'], 121e75a33bfSAndreas Gohr [ 122e75a33bfSAndreas Gohr 'currentID' => $current, 123e75a33bfSAndreas Gohr 'usetitle' => $useTitle, 124e75a33bfSAndreas Gohr ], 125*d418c031SAndreas Gohr $nspath, 126e75a33bfSAndreas Gohr 1, 127e75a33bfSAndreas Gohr '' // no sorting, we do ourselves 128e75a33bfSAndreas Gohr ); 12974c26ce5SAndreas Gohr if (!$items) return []; 130e75a33bfSAndreas Gohr 131e75a33bfSAndreas Gohr // split into separate levels 132e75a33bfSAndreas Gohr $parents = []; 133e75a33bfSAndreas Gohr $levels = []; 134*d418c031SAndreas Gohr $curLevel = $minlevel; 135e75a33bfSAndreas Gohr foreach ($items as $idx => $item) { 136*d418c031SAndreas Gohr if ($curLevel < $item['level']) { 137e75a33bfSAndreas Gohr // previous item was the parent 138*d418c031SAndreas Gohr $parents[] = array_key_last($levels[$curLevel]); 139e75a33bfSAndreas Gohr } 140*d418c031SAndreas Gohr $curLevel = $item['level']; 141e75a33bfSAndreas Gohr $levels[$item['level']][$idx] = $item; 142e75a33bfSAndreas Gohr } 143e75a33bfSAndreas Gohr 144e75a33bfSAndreas Gohr // sort each level separately 145e75a33bfSAndreas Gohr foreach ($levels as $level => $items) { 146*d418c031SAndreas Gohr uasort($items, fn($a, $b) => $this->itemComparator($a, $b, $useNatSort, $nsFirst)); 147e75a33bfSAndreas Gohr $levels[$level] = $items; 148e75a33bfSAndreas Gohr } 149e75a33bfSAndreas Gohr 150e75a33bfSAndreas Gohr // merge levels into a flat list again 151e75a33bfSAndreas Gohr $levels = array_reverse($levels, true); 152*d418c031SAndreas Gohr foreach (array_keys($levels) as $level) { 153*d418c031SAndreas Gohr if ($level == $minlevel) break; 154e75a33bfSAndreas Gohr 155e75a33bfSAndreas Gohr $parent = array_pop($parents); 156e75a33bfSAndreas Gohr $pos = array_search($parent, array_keys($levels[$level - 1])) + 1; 157e75a33bfSAndreas Gohr 158e58e2f72SAndreas Gohr /** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */ 159e75a33bfSAndreas Gohr $levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) + 160e75a33bfSAndreas Gohr $levels[$level] + 161e75a33bfSAndreas Gohr array_slice($levels[$level - 1], $pos, null, true); 162e75a33bfSAndreas Gohr } 163e75a33bfSAndreas Gohr 164*d418c031SAndreas Gohr return $levels[$minlevel]; 165e75a33bfSAndreas Gohr } 166e75a33bfSAndreas Gohr 167e75a33bfSAndreas Gohr /** 168e75a33bfSAndreas Gohr * Compare two items 169e75a33bfSAndreas Gohr * 170e75a33bfSAndreas Gohr * @param array $a 171e75a33bfSAndreas Gohr * @param array $b 172e75a33bfSAndreas Gohr * @param bool $useNatSort 173e58e2f72SAndreas Gohr * @param bool $nsFirst 174e75a33bfSAndreas Gohr * @return int 175e75a33bfSAndreas Gohr */ 176e58e2f72SAndreas Gohr public function itemComparator($a, $b, $useNatSort, $nsFirst) 177e75a33bfSAndreas Gohr { 178e58e2f72SAndreas Gohr if ($nsFirst && $a['type'] != $b['type']) { 179e58e2f72SAndreas Gohr return $a['type'] == 'd' ? -1 : 1; 180e58e2f72SAndreas Gohr } 181e58e2f72SAndreas Gohr 182e75a33bfSAndreas Gohr if ($useNatSort) { 183e75a33bfSAndreas Gohr return Sort::strcmp($a['title'], $b['title']); 184e75a33bfSAndreas Gohr } else { 185e75a33bfSAndreas Gohr return strcmp($a['title'], $b['title']); 186e75a33bfSAndreas Gohr } 187e75a33bfSAndreas Gohr } 188e75a33bfSAndreas Gohr 189e75a33bfSAndreas Gohr 190e75a33bfSAndreas Gohr /** 191d8ce5486SAndreas Gohr * Create a list openening 192d8ce5486SAndreas Gohr * 193d8ce5486SAndreas Gohr * @param array $item 194d8ce5486SAndreas Gohr * @return string 195d8ce5486SAndreas Gohr * @see html_buildlist() 196d8ce5486SAndreas Gohr */ 197d8ce5486SAndreas Gohr public function cbList($item) 198d8ce5486SAndreas Gohr { 199492ddc4eSAndreas Gohr global $INFO; 200492ddc4eSAndreas Gohr 201492ddc4eSAndreas Gohr if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) { 202e75a33bfSAndreas Gohr return '<strong>' . html_wikilink(':' . $item['id'], $item['title']) . '</strong>'; 203492ddc4eSAndreas Gohr } else { 204e75a33bfSAndreas Gohr return html_wikilink(':' . $item['id'], $item['title']); 205492ddc4eSAndreas Gohr } 2061169a1acSAndreas Gohr } 2071169a1acSAndreas Gohr 208d8ce5486SAndreas Gohr /** 209d8ce5486SAndreas Gohr * Create a list item 210d8ce5486SAndreas Gohr * 211d8ce5486SAndreas Gohr * @param array $item 212d8ce5486SAndreas Gohr * @return string 213d8ce5486SAndreas Gohr * @see html_buildlist() 214d8ce5486SAndreas Gohr */ 215d8ce5486SAndreas Gohr public function cbListItem($item) 216d8ce5486SAndreas Gohr { 2171169a1acSAndreas Gohr if ($item['type'] == "f") { 2181169a1acSAndreas Gohr return '<li class="level' . $item['level'] . '">'; 2191169a1acSAndreas Gohr } elseif ($item['open']) { 2201169a1acSAndreas Gohr return '<li class="open">'; 2211169a1acSAndreas Gohr } else { 2221169a1acSAndreas Gohr return '<li class="closed">'; 2231169a1acSAndreas Gohr } 2241169a1acSAndreas Gohr } 2251169a1acSAndreas Gohr 226d8ce5486SAndreas Gohr /** 227d8ce5486SAndreas Gohr * Custom search callback 228d8ce5486SAndreas Gohr * 229d8ce5486SAndreas Gohr * @param $data 230d8ce5486SAndreas Gohr * @param $base 231d8ce5486SAndreas Gohr * @param $file 232d8ce5486SAndreas Gohr * @param $type 233d8ce5486SAndreas Gohr * @param $lvl 234e75a33bfSAndreas Gohr * @param array $opts - currentID is the currently shown page 235d8ce5486SAndreas Gohr * @return bool 236d8ce5486SAndreas Gohr */ 237d8ce5486SAndreas Gohr public function cbSearch(&$data, $base, $file, $type, $lvl, $opts) 238d8ce5486SAndreas Gohr { 2391169a1acSAndreas Gohr global $conf; 2401169a1acSAndreas Gohr $return = true; 2411169a1acSAndreas Gohr 2421169a1acSAndreas Gohr $id = pathID($file); 2431169a1acSAndreas Gohr 244*d418c031SAndreas Gohr if ( 245*d418c031SAndreas Gohr $type == 'd' && !( 246e75a33bfSAndreas Gohr preg_match('#^' . $id . '(:|$)#', $opts['currentID']) || 247e75a33bfSAndreas Gohr preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID'])) 2481169a1acSAndreas Gohr 249*d418c031SAndreas Gohr ) 250*d418c031SAndreas Gohr ) { 2511169a1acSAndreas Gohr //add but don't recurse 2521169a1acSAndreas Gohr $return = false; 253303e1405SMichael Große } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) { 2541169a1acSAndreas Gohr //don't add 2551169a1acSAndreas Gohr return false; 2561169a1acSAndreas Gohr } 2571169a1acSAndreas Gohr 258660b56c3SAndreas Gohr // for sneaky index, check access to the namespace's start page 259660b56c3SAndreas Gohr if ($type == 'd' && $conf['sneaky_index']) { 260660b56c3SAndreas Gohr $sp = (new PageResolver(''))->resolveId($id . ':'); 261660b56c3SAndreas Gohr if (auth_quickaclcheck($sp) < AUTH_READ) { 2621169a1acSAndreas Gohr return false; 2631169a1acSAndreas Gohr } 264660b56c3SAndreas Gohr } 2651169a1acSAndreas Gohr 2661169a1acSAndreas Gohr if ($type == 'd') { 2671169a1acSAndreas Gohr // link directories to their start pages 268e75a33bfSAndreas Gohr $original = $id; 2691169a1acSAndreas Gohr $id = "$id:"; 270d8ce5486SAndreas Gohr $id = (new PageResolver(''))->resolveId($id); 2711169a1acSAndreas Gohr $this->startpages[$id] = 1; 272e75a33bfSAndreas Gohr 273e75a33bfSAndreas Gohr // if the resolve id is in the same namespace as the original it's a start page named like the dir 274*d418c031SAndreas Gohr if (getNS($original) === getNS($id)) { 275e75a33bfSAndreas Gohr $useNS = $original; 276e75a33bfSAndreas Gohr } 277303e1405SMichael Große } elseif (!empty($this->startpages[$id])) { 2781169a1acSAndreas Gohr // skip already shown start pages 2791169a1acSAndreas Gohr return false; 2801169a1acSAndreas Gohr } 2811169a1acSAndreas Gohr 2821169a1acSAndreas Gohr //check hidden 2831169a1acSAndreas Gohr if (isHiddenPage($id)) { 2841169a1acSAndreas Gohr return false; 2851169a1acSAndreas Gohr } 2861169a1acSAndreas Gohr 2871169a1acSAndreas Gohr //check ACL 2881169a1acSAndreas Gohr if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) { 2891169a1acSAndreas Gohr return false; 2901169a1acSAndreas Gohr } 2911169a1acSAndreas Gohr 292e75a33bfSAndreas Gohr $data[$id] = [ 293d8ce5486SAndreas Gohr 'id' => $id, 2941169a1acSAndreas Gohr 'type' => $type, 2951169a1acSAndreas Gohr 'level' => $lvl, 296d8ce5486SAndreas Gohr 'open' => $return, 297e75a33bfSAndreas Gohr 'title' => $this->getTitle($id, $opts['usetitle']), 298e75a33bfSAndreas Gohr 'ns' => $useNS ?? (string)getNS($id), 299e75a33bfSAndreas Gohr ]; 300e75a33bfSAndreas Gohr 3011169a1acSAndreas Gohr return $return; 3021169a1acSAndreas Gohr } 3031169a1acSAndreas Gohr 304d8ce5486SAndreas Gohr /** 305*d418c031SAndreas Gohr * @param string $id 306*d418c031SAndreas Gohr * @param bool $useTitle 307*d418c031SAndreas Gohr * @return array 308*d418c031SAndreas Gohr */ 309*d418c031SAndreas Gohr protected function getMainStartPage($ns, $useTitle) 310*d418c031SAndreas Gohr { 311*d418c031SAndreas Gohr $resolver = new PageResolver(''); 312*d418c031SAndreas Gohr $id = $resolver->resolveId($ns . ':'); 313*d418c031SAndreas Gohr 314*d418c031SAndreas Gohr $item = [ 315*d418c031SAndreas Gohr 'id' => $id, 316*d418c031SAndreas Gohr 'type' => 'd', 317*d418c031SAndreas Gohr 'level' => 0, 318*d418c031SAndreas Gohr 'open' => true, 319*d418c031SAndreas Gohr 'title' => $this->getTitle($id, $useTitle), 320*d418c031SAndreas Gohr 'ns' => $ns, 321*d418c031SAndreas Gohr ]; 322*d418c031SAndreas Gohr $this->startpages[$id] = 1; 323*d418c031SAndreas Gohr return $item; 324*d418c031SAndreas Gohr } 325*d418c031SAndreas Gohr 326*d418c031SAndreas Gohr /** 327d8ce5486SAndreas Gohr * Get the title for the given page ID 328d8ce5486SAndreas Gohr * 329d8ce5486SAndreas Gohr * @param string $id 330e75a33bfSAndreas Gohr * @param bool $usetitle - use the first heading as title 331d8ce5486SAndreas Gohr * @return string 332d8ce5486SAndreas Gohr */ 333e75a33bfSAndreas Gohr protected function getTitle($id, $usetitle) 334d8ce5486SAndreas Gohr { 335e306992cSAndreas Gohr global $conf; 336e306992cSAndreas Gohr 337e75a33bfSAndreas Gohr if ($usetitle) { 338e306992cSAndreas Gohr $p = p_get_first_heading($id); 339303e1405SMichael Große if (!empty($p)) return $p; 340e75a33bfSAndreas Gohr } 341e306992cSAndreas Gohr 342e306992cSAndreas Gohr $p = noNS($id); 343d8ce5486SAndreas Gohr if ($p == $conf['start'] || !$p) { 344e306992cSAndreas Gohr $p = noNS(getNS($id)); 345d8ce5486SAndreas Gohr if (!$p) { 346e306992cSAndreas Gohr return $conf['start']; 347e306992cSAndreas Gohr } 348e306992cSAndreas Gohr } 349e306992cSAndreas Gohr return $p; 350e306992cSAndreas Gohr } 3511169a1acSAndreas Gohr} 352