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