1<?php 2 3use dokuwiki\File\PageResolver; 4use dokuwiki\Utf8\Sort; 5 6/** 7 * DokuWiki Plugin simplenavi (Syntax Component) 8 * 9 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 10 * @author Andreas Gohr <gohr@cosmocode.de> 11 */ 12class syntax_plugin_simplenavi extends DokuWiki_Syntax_Plugin 13{ 14 private $startpages = []; 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('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 38 } 39 40 /** @inheritdoc */ 41 public function handle($match, $state, $pos, Doku_Handler $handler) 42 { 43 return explode(' ', substr($match, 13, -2)); 44 } 45 46 /** @inheritdoc */ 47 public function render($format, Doku_Renderer $renderer, $data) 48 { 49 if ($format != 'xhtml') return false; 50 51 global $INFO; 52 $renderer->nocache(); 53 54 // first data is namespace, rest is options 55 $ns = array_shift($data); 56 if ($ns && $ns[0] === '.') { 57 // resolve relative to current page 58 $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 59 } else { 60 $ns = cleanID($ns); 61 } 62 // convert to path 63 $ns = utf8_encodeFN(str_replace(':', '/', $ns)); 64 65 $items = $this->getSortedItems( 66 $ns, 67 $INFO['id'], 68 $this->getConf('usetitle'), 69 $this->getConf('natsort'), 70 $this->getConf('nsfirst') 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 * @return array 93 */ 94 public function getSortedItems($ns, $current, $useTitle, $useNatSort, $nsFirst) 95 { 96 global $conf; 97 98 // execute search using our own callback 99 $items = []; 100 search( 101 $items, 102 $conf['datadir'], 103 [$this, 'cbSearch'], 104 [ 105 'currentID' => $current, 106 'usetitle' => $useTitle, 107 ], 108 $ns, 109 1, 110 '' // no sorting, we do ourselves 111 ); 112 if(!$items) return []; 113 114 // split into separate levels 115 $current = 1; 116 $parents = []; 117 $levels = []; 118 foreach ($items as $idx => $item) { 119 if ($current < $item['level']) { 120 // previous item was the parent 121 $parents[] = array_key_last($levels[$current]); 122 } 123 $current = $item['level']; 124 $levels[$item['level']][$idx] = $item; 125 } 126 127 // sort each level separately 128 foreach ($levels as $level => $items) { 129 uasort($items, function ($a, $b) use ($useNatSort, $nsFirst) { 130 return $this->itemComparator($a, $b, $useNatSort, $nsFirst); 131 }); 132 $levels[$level] = $items; 133 } 134 135 // merge levels into a flat list again 136 $levels = array_reverse($levels, true); 137 foreach ($levels as $level => $items) { 138 if ($level == 1) break; 139 140 $parent = array_pop($parents); 141 $pos = array_search($parent, array_keys($levels[$level - 1])) + 1; 142 143 /** @noinspection PhpArrayAccessCanBeReplacedWithForeachValueInspection */ 144 $levels[$level - 1] = array_slice($levels[$level - 1], 0, $pos, true) + 145 $levels[$level] + 146 array_slice($levels[$level - 1], $pos, null, true); 147 } 148 149 return $levels[1]; 150 } 151 152 /** 153 * Compare two items 154 * 155 * @param array $a 156 * @param array $b 157 * @param bool $useNatSort 158 * @param bool $nsFirst 159 * @return int 160 */ 161 public function itemComparator($a, $b, $useNatSort, $nsFirst) 162 { 163 if ($nsFirst && $a['type'] != $b['type']) { 164 return $a['type'] == 'd' ? -1 : 1; 165 } 166 167 if ($useNatSort) { 168 return Sort::strcmp($a['title'], $b['title']); 169 } else { 170 return strcmp($a['title'], $b['title']); 171 } 172 } 173 174 175 /** 176 * Create a list openening 177 * 178 * @param array $item 179 * @return string 180 * @see html_buildlist() 181 */ 182 public function cbList($item) 183 { 184 global $INFO; 185 186 if (($item['type'] == 'd' && $item['open']) || $INFO['id'] == $item['id']) { 187 return '<strong>' . html_wikilink(':' . $item['id'], $item['title']) . '</strong>'; 188 } else { 189 return html_wikilink(':' . $item['id'], $item['title']); 190 } 191 192 } 193 194 /** 195 * Create a list item 196 * 197 * @param array $item 198 * @return string 199 * @see html_buildlist() 200 */ 201 public function cbListItem($item) 202 { 203 if ($item['type'] == "f") { 204 return '<li class="level' . $item['level'] . '">'; 205 } elseif ($item['open']) { 206 return '<li class="open">'; 207 } else { 208 return '<li class="closed">'; 209 } 210 } 211 212 /** 213 * Custom search callback 214 * 215 * @param $data 216 * @param $base 217 * @param $file 218 * @param $type 219 * @param $lvl 220 * @param array $opts - currentID is the currently shown page 221 * @return bool 222 */ 223 public function cbSearch(&$data, $base, $file, $type, $lvl, $opts) 224 { 225 global $conf; 226 $return = true; 227 228 $id = pathID($file); 229 230 if ($type == 'd' && !( 231 preg_match('#^' . $id . '(:|$)#', $opts['currentID']) || 232 preg_match('#^' . $id . '(:|$)#', getNS($opts['currentID'])) 233 234 )) { 235 //add but don't recurse 236 $return = false; 237 } elseif ($type == 'f' && (!empty($opts['nofiles']) || substr($file, -4) != '.txt')) { 238 //don't add 239 return false; 240 } 241 242 // for sneaky index, check access to the namespace's start page 243 if ($type == 'd' && $conf['sneaky_index']) { 244 $sp = (new PageResolver(''))->resolveId($id . ':'); 245 if (auth_quickaclcheck($sp) < AUTH_READ) { 246 return false; 247 } 248 } 249 250 if ($type == 'd') { 251 // link directories to their start pages 252 $original = $id; 253 $id = "$id:"; 254 $id = (new PageResolver(''))->resolveId($id); 255 $this->startpages[$id] = 1; 256 257 // if the resolve id is in the same namespace as the original it's a start page named like the dir 258 if (getNS($original) == getNS($id)) { 259 $useNS = $original; 260 } 261 262 } elseif (!empty($this->startpages[$id])) { 263 // skip already shown start pages 264 return false; 265 } elseif (noNS($id) == $conf['start']) { 266 // skip the main start page 267 return false; 268 } 269 270 //check hidden 271 if (isHiddenPage($id)) { 272 return false; 273 } 274 275 //check ACL 276 if ($type == 'f' && auth_quickaclcheck($id) < AUTH_READ) { 277 return false; 278 } 279 280 $data[$id] = [ 281 'id' => $id, 282 'type' => $type, 283 'level' => $lvl, 284 'open' => $return, 285 'title' => $this->getTitle($id, $opts['usetitle']), 286 'ns' => $useNS ?? (string)getNS($id), 287 ]; 288 289 return $return; 290 } 291 292 /** 293 * Get the title for the given page ID 294 * 295 * @param string $id 296 * @param bool $usetitle - use the first heading as title 297 * @return string 298 */ 299 protected function getTitle($id, $usetitle) 300 { 301 global $conf; 302 303 if ($usetitle) { 304 $p = p_get_first_heading($id); 305 if (!empty($p)) return $p; 306 } 307 308 $p = noNS($id); 309 if ($p == $conf['start'] || !$p) { 310 $p = noNS(getNS($id)); 311 if (!$p) { 312 return $conf['start']; 313 } 314 } 315 return $p; 316 } 317} 318