11169a1acSAndreas Gohr<?php 2d8ce5486SAndreas Gohr 3d8ce5486SAndreas Gohruse dokuwiki\File\PageResolver; 4e75a33bfSAndreas Gohruse dokuwiki\Utf8\Sort; 5d8ce5486SAndreas Gohr 61169a1acSAndreas Gohr/** 71169a1acSAndreas Gohr * DokuWiki Plugin simplenavi (Syntax Component) 81169a1acSAndreas Gohr * 91169a1acSAndreas Gohr * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 101169a1acSAndreas Gohr * @author Andreas Gohr <gohr@cosmocode.de> 111169a1acSAndreas Gohr */ 12d8ce5486SAndreas Gohrclass syntax_plugin_simplenavi extends DokuWiki_Syntax_Plugin 13d8ce5486SAndreas Gohr{ 14d8ce5486SAndreas Gohr private $startpages = []; 151169a1acSAndreas Gohr 16d8ce5486SAndreas Gohr /** @inheritdoc */ 17d8ce5486SAndreas Gohr public function getType() 18d8ce5486SAndreas Gohr { 191169a1acSAndreas Gohr return 'substition'; 201169a1acSAndreas Gohr } 211169a1acSAndreas Gohr 22d8ce5486SAndreas Gohr /** @inheritdoc */ 23d8ce5486SAndreas Gohr public function getPType() 24d8ce5486SAndreas Gohr { 251169a1acSAndreas Gohr return 'block'; 261169a1acSAndreas Gohr } 271169a1acSAndreas Gohr 28d8ce5486SAndreas Gohr /** @inheritdoc */ 29d8ce5486SAndreas Gohr public function getSort() 30d8ce5486SAndreas Gohr { 311169a1acSAndreas Gohr return 155; 321169a1acSAndreas Gohr } 331169a1acSAndreas Gohr 34d8ce5486SAndreas Gohr /** @inheritdoc */ 35d8ce5486SAndreas Gohr public function connectTo($mode) 36d8ce5486SAndreas Gohr { 371169a1acSAndreas Gohr $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 381169a1acSAndreas Gohr } 391169a1acSAndreas Gohr 40d8ce5486SAndreas Gohr /** @inheritdoc */ 41d8ce5486SAndreas Gohr public function handle($match, $state, $pos, Doku_Handler $handler) 42d8ce5486SAndreas Gohr { 435655937aSAndreas Gohr return explode(' ', substr($match, 13, -2)); 441169a1acSAndreas Gohr } 451169a1acSAndreas Gohr 46d8ce5486SAndreas Gohr /** @inheritdoc */ 47d8ce5486SAndreas Gohr public function render($format, Doku_Renderer $renderer, $data) 48d8ce5486SAndreas Gohr { 49d8ce5486SAndreas Gohr if ($format != 'xhtml') return false; 501169a1acSAndreas Gohr 511169a1acSAndreas Gohr global $INFO; 52b3e02951SAndreas Gohr $renderer->nocache(); 531169a1acSAndreas Gohr 54b3e02951SAndreas Gohr // first data is namespace, rest is options 555655937aSAndreas Gohr $ns = array_shift($data); 565655937aSAndreas Gohr if ($ns && $ns[0] === '.') { 575655937aSAndreas Gohr // resolve relative to current page 585655937aSAndreas Gohr $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 595655937aSAndreas Gohr } else { 605655937aSAndreas Gohr $ns = cleanID($ns); 615655937aSAndreas Gohr } 625655937aSAndreas Gohr // convert to path 635655937aSAndreas Gohr $ns = utf8_encodeFN(str_replace(':', '/', $ns)); 64b3e02951SAndreas Gohr 65e75a33bfSAndreas Gohr $items = $this->getSortedItems( 66e75a33bfSAndreas Gohr $ns, 67e75a33bfSAndreas Gohr $INFO['id'], 68e75a33bfSAndreas Gohr $this->getConf('usetitle'), 69e58e2f72SAndreas Gohr $this->getConf('natsort'), 70e58e2f72SAndreas Gohr $this->getConf('nsfirst') 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? 92e75a33bfSAndreas Gohr * @return array 93e75a33bfSAndreas Gohr */ 94e58e2f72SAndreas Gohr public function getSortedItems($ns, $current, $useTitle, $useNatSort, $nsFirst) 95e75a33bfSAndreas Gohr { 96e75a33bfSAndreas Gohr global $conf; 97e75a33bfSAndreas Gohr 98e75a33bfSAndreas Gohr // execute search using our own callback 99e75a33bfSAndreas Gohr $items = []; 100e75a33bfSAndreas Gohr search( 101e75a33bfSAndreas Gohr $items, 102e75a33bfSAndreas Gohr $conf['datadir'], 103e75a33bfSAndreas Gohr [$this, 'cbSearch'], 104e75a33bfSAndreas Gohr [ 105e75a33bfSAndreas Gohr 'currentID' => $current, 106e75a33bfSAndreas Gohr 'usetitle' => $useTitle, 107e75a33bfSAndreas Gohr ], 108e75a33bfSAndreas Gohr $ns, 109e75a33bfSAndreas Gohr 1, 110e75a33bfSAndreas Gohr '' // no sorting, we do ourselves 111e75a33bfSAndreas Gohr ); 112*74c26ce5SAndreas Gohr if(!$items) return []; 113e75a33bfSAndreas Gohr 114e75a33bfSAndreas Gohr // split into separate levels 115e75a33bfSAndreas Gohr $current = 1; 116e75a33bfSAndreas Gohr $parents = []; 117e75a33bfSAndreas Gohr $levels = []; 118e75a33bfSAndreas Gohr foreach ($items as $idx => $item) { 119e75a33bfSAndreas Gohr if ($current < $item['level']) { 120e75a33bfSAndreas Gohr // previous item was the parent 121e75a33bfSAndreas Gohr $parents[] = array_key_last($levels[$current]); 122e75a33bfSAndreas Gohr } 123e75a33bfSAndreas Gohr $current = $item['level']; 124e75a33bfSAndreas Gohr $levels[$item['level']][$idx] = $item; 125e75a33bfSAndreas Gohr } 126e75a33bfSAndreas Gohr 127e75a33bfSAndreas Gohr // sort each level separately 128e75a33bfSAndreas Gohr foreach ($levels as $level => $items) { 129e58e2f72SAndreas Gohr uasort($items, function ($a, $b) use ($useNatSort, $nsFirst) { 130e58e2f72SAndreas Gohr return $this->itemComparator($a, $b, $useNatSort, $nsFirst); 131e75a33bfSAndreas Gohr }); 132e75a33bfSAndreas Gohr $levels[$level] = $items; 133e75a33bfSAndreas Gohr } 134e75a33bfSAndreas Gohr 135e75a33bfSAndreas Gohr // merge levels into a flat list again 136e75a33bfSAndreas Gohr $levels = array_reverse($levels, true); 137e75a33bfSAndreas Gohr foreach ($levels as $level => $items) { 138e75a33bfSAndreas Gohr if ($level == 1) break; 139e75a33bfSAndreas Gohr 140e75a33bfSAndreas Gohr $parent = array_pop($parents); 141e75a33bfSAndreas Gohr $pos = array_search($parent, array_keys($levels[$level - 1])) + 1; 142e75a33bfSAndreas Gohr 143e58e2f72SAndreas Gohr /** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */ 144e75a33bfSAndreas Gohr $levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) + 145e75a33bfSAndreas Gohr $levels[$level] + 146e75a33bfSAndreas Gohr array_slice($levels[$level - 1], $pos, null, true); 147e75a33bfSAndreas Gohr } 148e75a33bfSAndreas Gohr 149e58e2f72SAndreas Gohr return $levels[1]; 150e75a33bfSAndreas Gohr } 151e75a33bfSAndreas Gohr 152e75a33bfSAndreas Gohr /** 153e75a33bfSAndreas Gohr * Compare two items 154e75a33bfSAndreas Gohr * 155e75a33bfSAndreas Gohr * @param array $a 156e75a33bfSAndreas Gohr * @param array $b 157e75a33bfSAndreas Gohr * @param bool $useNatSort 158e58e2f72SAndreas Gohr * @param bool $nsFirst 159e75a33bfSAndreas Gohr * @return int 160e75a33bfSAndreas Gohr */ 161e58e2f72SAndreas Gohr public function itemComparator($a, $b, $useNatSort, $nsFirst) 162e75a33bfSAndreas Gohr { 163e58e2f72SAndreas Gohr if ($nsFirst && $a['type'] != $b['type']) { 164e58e2f72SAndreas Gohr return $a['type'] == 'd' ? -1 : 1; 165e58e2f72SAndreas Gohr } 166e58e2f72SAndreas Gohr 167e75a33bfSAndreas Gohr if ($useNatSort) { 168e75a33bfSAndreas Gohr return Sort::strcmp($a['title'], $b['title']); 169e75a33bfSAndreas Gohr } else { 170e75a33bfSAndreas Gohr return strcmp($a['title'], $b['title']); 171e75a33bfSAndreas Gohr } 172e75a33bfSAndreas Gohr } 173e75a33bfSAndreas Gohr 174e75a33bfSAndreas Gohr 175e75a33bfSAndreas Gohr /** 176d8ce5486SAndreas Gohr * Create a list openening 177d8ce5486SAndreas Gohr * 178d8ce5486SAndreas Gohr * @param array $item 179d8ce5486SAndreas Gohr * @return string 180d8ce5486SAndreas Gohr * @see html_buildlist() 181d8ce5486SAndreas Gohr */ 182d8ce5486SAndreas Gohr public function cbList($item) 183d8ce5486SAndreas Gohr { 184492ddc4eSAndreas Gohr global $INFO; 185492ddc4eSAndreas Gohr 186492ddc4eSAndreas Gohr if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) { 187e75a33bfSAndreas Gohr return '<strong>' . html_wikilink(':' . $item['id'], $item['title']) . '</strong>'; 188492ddc4eSAndreas Gohr } else { 189e75a33bfSAndreas Gohr return html_wikilink(':' . $item['id'], $item['title']); 190492ddc4eSAndreas Gohr } 1911169a1acSAndreas Gohr 1921169a1acSAndreas Gohr } 1931169a1acSAndreas Gohr 194d8ce5486SAndreas Gohr /** 195d8ce5486SAndreas Gohr * Create a list item 196d8ce5486SAndreas Gohr * 197d8ce5486SAndreas Gohr * @param array $item 198d8ce5486SAndreas Gohr * @return string 199d8ce5486SAndreas Gohr * @see html_buildlist() 200d8ce5486SAndreas Gohr */ 201d8ce5486SAndreas Gohr public function cbListItem($item) 202d8ce5486SAndreas Gohr { 2031169a1acSAndreas Gohr if ($item['type'] == "f") { 2041169a1acSAndreas Gohr return '<li class="level' . $item['level'] . '">'; 2051169a1acSAndreas Gohr } elseif ($item['open']) { 2061169a1acSAndreas Gohr return '<li class="open">'; 2071169a1acSAndreas Gohr } else { 2081169a1acSAndreas Gohr return '<li class="closed">'; 2091169a1acSAndreas Gohr } 2101169a1acSAndreas Gohr } 2111169a1acSAndreas Gohr 212d8ce5486SAndreas Gohr /** 213d8ce5486SAndreas Gohr * Custom search callback 214d8ce5486SAndreas Gohr * 215d8ce5486SAndreas Gohr * @param $data 216d8ce5486SAndreas Gohr * @param $base 217d8ce5486SAndreas Gohr * @param $file 218d8ce5486SAndreas Gohr * @param $type 219d8ce5486SAndreas Gohr * @param $lvl 220e75a33bfSAndreas Gohr * @param array $opts - currentID is the currently shown page 221d8ce5486SAndreas Gohr * @return bool 222d8ce5486SAndreas Gohr */ 223d8ce5486SAndreas Gohr public function cbSearch(&$data, $base, $file, $type, $lvl, $opts) 224d8ce5486SAndreas Gohr { 2251169a1acSAndreas Gohr global $conf; 2261169a1acSAndreas Gohr $return = true; 2271169a1acSAndreas Gohr 2281169a1acSAndreas Gohr $id = pathID($file); 2291169a1acSAndreas Gohr 2301169a1acSAndreas Gohr if ($type == 'd' && !( 231e75a33bfSAndreas Gohr preg_match('#^' . $id . '(:|$)#', $opts['currentID']) || 232e75a33bfSAndreas Gohr preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID'])) 2331169a1acSAndreas Gohr 2341169a1acSAndreas Gohr )) { 2351169a1acSAndreas Gohr //add but don't recurse 2361169a1acSAndreas Gohr $return = false; 237303e1405SMichael Große } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) { 2381169a1acSAndreas Gohr //don't add 2391169a1acSAndreas Gohr return false; 2401169a1acSAndreas Gohr } 2411169a1acSAndreas Gohr 242660b56c3SAndreas Gohr // for sneaky index, check access to the namespace's start page 243660b56c3SAndreas Gohr if ($type == 'd' && $conf['sneaky_index']) { 244660b56c3SAndreas Gohr $sp = (new PageResolver(''))->resolveId($id . ':'); 245660b56c3SAndreas Gohr if (auth_quickaclcheck($sp) < AUTH_READ) { 2461169a1acSAndreas Gohr return false; 2471169a1acSAndreas Gohr } 248660b56c3SAndreas Gohr } 2491169a1acSAndreas Gohr 2501169a1acSAndreas Gohr if ($type == 'd') { 2511169a1acSAndreas Gohr // link directories to their start pages 252e75a33bfSAndreas Gohr $original = $id; 2531169a1acSAndreas Gohr $id = "$id:"; 254d8ce5486SAndreas Gohr $id = (new PageResolver(''))->resolveId($id); 2551169a1acSAndreas Gohr $this->startpages[$id] = 1; 256e75a33bfSAndreas Gohr 257e75a33bfSAndreas Gohr // if the resolve id is in the same namespace as the original it's a start page named like the dir 258e75a33bfSAndreas Gohr if (getNS($original) == getNS($id)) { 259e75a33bfSAndreas Gohr $useNS = $original; 260e75a33bfSAndreas Gohr } 261e75a33bfSAndreas Gohr 262303e1405SMichael Große } elseif (!empty($this->startpages[$id])) { 2631169a1acSAndreas Gohr // skip already shown start pages 2641169a1acSAndreas Gohr return false; 2651169a1acSAndreas Gohr } elseif (noNS($id) == $conf['start']) { 2661169a1acSAndreas Gohr // skip the main start page 2671169a1acSAndreas Gohr return false; 2681169a1acSAndreas Gohr } 2691169a1acSAndreas Gohr 2701169a1acSAndreas Gohr //check hidden 2711169a1acSAndreas Gohr if (isHiddenPage($id)) { 2721169a1acSAndreas Gohr return false; 2731169a1acSAndreas Gohr } 2741169a1acSAndreas Gohr 2751169a1acSAndreas Gohr //check ACL 2761169a1acSAndreas Gohr if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) { 2771169a1acSAndreas Gohr return false; 2781169a1acSAndreas Gohr } 2791169a1acSAndreas Gohr 280e75a33bfSAndreas Gohr $data[$id] = [ 281d8ce5486SAndreas Gohr 'id' => $id, 2821169a1acSAndreas Gohr 'type' => $type, 2831169a1acSAndreas Gohr 'level' => $lvl, 284d8ce5486SAndreas Gohr 'open' => $return, 285e75a33bfSAndreas Gohr 'title' => $this->getTitle($id, $opts['usetitle']), 286e75a33bfSAndreas Gohr 'ns' => $useNS ?? (string)getNS($id), 287e75a33bfSAndreas Gohr ]; 288e75a33bfSAndreas Gohr 2891169a1acSAndreas Gohr return $return; 2901169a1acSAndreas Gohr } 2911169a1acSAndreas Gohr 292d8ce5486SAndreas Gohr /** 293d8ce5486SAndreas Gohr * Get the title for the given page ID 294d8ce5486SAndreas Gohr * 295d8ce5486SAndreas Gohr * @param string $id 296e75a33bfSAndreas Gohr * @param bool $usetitle - use the first heading as title 297d8ce5486SAndreas Gohr * @return string 298d8ce5486SAndreas Gohr */ 299e75a33bfSAndreas Gohr protected function getTitle($id, $usetitle) 300d8ce5486SAndreas Gohr { 301e306992cSAndreas Gohr global $conf; 302e306992cSAndreas Gohr 303e75a33bfSAndreas Gohr if ($usetitle) { 304e306992cSAndreas Gohr $p = p_get_first_heading($id); 305303e1405SMichael Große if (!empty($p)) return $p; 306e75a33bfSAndreas Gohr } 307e306992cSAndreas Gohr 308e306992cSAndreas Gohr $p = noNS($id); 309d8ce5486SAndreas Gohr if ($p == $conf['start'] || !$p) { 310e306992cSAndreas Gohr $p = noNS(getNS($id)); 311d8ce5486SAndreas Gohr if (!$p) { 312e306992cSAndreas Gohr return $conf['start']; 313e306992cSAndreas Gohr } 314e306992cSAndreas Gohr } 315e306992cSAndreas Gohr return $p; 316e306992cSAndreas Gohr } 3171169a1acSAndreas Gohr} 318