1<?php 2 3use dokuwiki\Extension\SyntaxPlugin; 4use dokuwiki\File\PageResolver; 5use dokuwiki\TreeBuilder\Node\AbstractNode; 6use dokuwiki\TreeBuilder\Node\WikiNamespace; 7use dokuwiki\TreeBuilder\Node\WikiStartpage; 8use dokuwiki\TreeBuilder\PageTreeBuilder; 9use dokuwiki\TreeBuilder\TreeSort; 10 11/** 12 * DokuWiki Plugin simplenavi (Syntax Component) 13 * 14 * @license GPL 2 http://www.gnu.org/licenses/gpl-2.0.html 15 * @author Andreas Gohr <gohr@cosmocode.de> 16 */ 17class syntax_plugin_simplenavi extends SyntaxPlugin 18{ 19 protected string $ns; 20 protected string $currentID; 21 protected bool $usetitle; 22 protected string $sort; 23 protected bool $home; 24 protected int $peek = 0; 25 protected bool $filter = false; 26 27 /** @inheritdoc */ 28 public function getType() 29 { 30 return 'substition'; 31 } 32 33 /** @inheritdoc */ 34 public function getPType() 35 { 36 return 'block'; 37 } 38 39 /** @inheritdoc */ 40 public function getSort() 41 { 42 return 155; 43 } 44 45 /** @inheritdoc */ 46 public function connectTo($mode) 47 { 48 $this->Lexer->addSpecialPattern('{{simplenavi>[^}]*}}', $mode, 'plugin_simplenavi'); 49 } 50 51 /** @inheritdoc */ 52 public function handle($match, $state, $pos, Doku_Handler $handler) 53 { 54 return explode(' ', substr($match, 13, -2)); 55 } 56 57 /** @inheritdoc */ 58 public function render($format, Doku_Renderer $renderer, $data) 59 { 60 if ($format != 'xhtml') return false; 61 $renderer->nocache(); 62 63 global $INFO; 64 65 // first data is namespace, rest is options 66 $ns = array_shift($data); 67 if ($ns && $ns[0] === '.') { 68 // resolve relative to current page 69 $ns = getNS((new PageResolver($INFO['id']))->resolveId("$ns:xxx")); 70 } else { 71 $ns = cleanID($ns); 72 } 73 74 $this->initState( 75 $ns, 76 $INFO['id'], 77 (bool)$this->getConf('usetitle'), 78 $this->getConf('sort'), 79 in_array('home', $data), 80 $this->getConf('peek', 0), 81 in_array('filter', $data) 82 ); 83 84 $tree = $this->getTree(); 85 86 $class = 'plugin__simplenavi'; 87 if ($this->filter) { 88 $class .= ' plugin__simplenavi_filter'; 89 } 90 91 $renderer->doc .= '<div class="' . $class . '">'; 92 $this->renderTree($renderer, $tree->getTop()); 93 $renderer->doc .= '</div>'; 94 95 return true; 96 } 97 98 /** 99 * Initialize the configuration state of the plugin 100 * 101 * Also used in testing 102 */ 103 public function initState( 104 string $ns, 105 string $currentID, 106 bool $usetitle, 107 string $sort, 108 bool $home, 109 int $peek = 0, 110 bool $filter = false 111 ) 112 { 113 $this->ns = $ns; 114 $this->currentID = $currentID; 115 $this->usetitle = $usetitle; 116 $this->sort = $sort; 117 $this->home = $home; 118 $this->peek = $peek; 119 $this->filter = $filter; 120 } 121 122 /** 123 * Create the tree 124 * 125 * @return PageTreeBuilder 126 */ 127 protected function getTree(): PageTreeBuilder 128 { 129 $tree = new PageTreeBuilder($this->ns); 130 $tree->addFlag(PageTreeBuilder::FLAG_NS_AS_STARTPAGE); 131 if ($this->home) $tree->addFlag(PageTreeBuilder::FLAG_SELF_TOP); 132 $tree->setRecursionDecision(\Closure::fromCallable([$this, 'treeRecursionDecision'])); 133 $tree->setNodeProcessor(\Closure::fromCallable([$this, 'treeNodeProcessor'])); 134 $tree->generate(); 135 136 switch ($this->sort) { 137 case 'id': 138 $tree->sort(TreeSort::SORT_BY_ID); 139 break; 140 case 'title': 141 $tree->sort(TreeSort::SORT_BY_TITLE); 142 break; 143 case 'ns_id': 144 $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_ID); 145 break; 146 default: 147 $tree->sort(TreeSort::SORT_BY_NS_FIRST_THEN_TITLE); 148 break; 149 } 150 151 return $tree; 152 } 153 154 155 /** 156 * Callback for the PageTreeBuilder to decide if we want to recurse into a node 157 * 158 * @param AbstractNode $node 159 * @param int $depth 160 * @return bool 161 */ 162 protected function treeRecursionDecision(AbstractNode $node, int $depth): bool 163 { 164 if ($node instanceof WikiStartpage) { 165 $id = $node->getNs(); // use the namespace for startpages 166 } else { 167 $id = $node->getId(); 168 } 169 170 $is_current = $this->isParent($this->currentID, $id); 171 $node->setProperty('is_current', $is_current); 172 173 // always recurse into the current page path 174 if ($is_current) return true; 175 176 // should we peek deeper to see if there's something readable? 177 if ($depth < $this->peek && auth_quickaclcheck($node->getId()) < AUTH_READ) { 178 return true; 179 } 180 181 return false; 182 } 183 184 /** 185 * Callback for the PageTreeBuilder to process a node 186 * 187 * @param AbstractNode $node 188 * @return AbstractNode|null 189 */ 190 protected function treeNodeProcessor(AbstractNode $node): ?AbstractNode 191 { 192 $perm = auth_quickaclcheck($node->getId()); 193 $node->setProperty('permission', $perm); 194 $node->setTitle($this->getTitle($node->getId())); 195 196 197 if ($node->hasChildren()) { 198 // this node has children, we add it to the tree regardless of the permission 199 // permissions are checked again when rendering 200 return $node; 201 } 202 203 if ($perm < AUTH_READ) { 204 // no children, no permission. No need to add it to the tree 205 return null; 206 } 207 208 // don't show hidden pages 209 if(isHiddenPage($node->getId())) return null; 210 211 return $node; 212 } 213 214 215 /** 216 * Render the tree 217 * 218 * @param Doku_Renderer $R The current renderer 219 * @param AbstractNode $top The top node of the tree (use getTop() to get it) 220 * @param int $level current nesting level, starting at 1 221 * @return void 222 */ 223 protected function renderTree(Doku_Renderer $R, AbstractNode $top, $level = 1) 224 { 225 $R->listu_open(); 226 foreach ($top->getChildren() as $node) { 227 $isfolder = $node instanceof WikiNamespace; 228 $incurrent = $node->getProperty('is_current', false); 229 230 $R->listitem_open(1, $isfolder); 231 $R->listcontent_open(); 232 if ($incurrent) $R->strong_open(); 233 234 if (((int)$node->getProperty('permission', 0)) < AUTH_READ) { 235 $R->cdata($node->getTitle()); 236 } else { 237 $R->internallink($node->getId(), $node->getTitle(), null, false, 'navigation'); 238 } 239 240 if ($incurrent) $R->strong_close(); 241 $R->listcontent_close(); 242 if ($node->hasChildren()) { 243 $this->renderTree($R, $node, $level + 1); 244 } 245 $R->listitem_close(); 246 } 247 $R->listu_close(); 248 } 249 250 /** 251 * Check if the given parent ID is a parent of the child ID 252 * 253 * @param string $child 254 * @param string $parent 255 * @return bool 256 */ 257 protected function isParent(string $child, string $parent) 258 { 259 // Empty parent is considered a parent of all pages 260 if ($parent === '') { 261 return true; 262 } 263 264 $child = explode(':', $child); 265 $parent = explode(':', $parent); 266 return array_slice($child, 0, count($parent)) === $parent; 267 } 268 269 270 /** 271 * Get the title for the given page ID 272 * 273 * @param string $id 274 * @return string 275 */ 276 protected function getTitle($id) 277 { 278 global $conf; 279 280 if ($this->usetitle) { 281 $p = p_get_first_heading($id); 282 if (!empty($p)) return $p; 283 } 284 285 $p = noNS($id); 286 if ($p == $conf['start'] || !$p) { 287 $p = noNS(getNS($id)); 288 if (!$p) { 289 return $conf['start']; 290 } 291 } 292 return $p; 293 } 294} 295