1<?php 2/** 3 * AcMenu plugin: an accordion menu for namespaces and relative pages. 4 * 5 * syntax.php: methods used by AcMenu plugin that extend DokuWiki syntax. 6 * 7 * @author Torpedo <dcstoyanov@gmail.com> 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @package syntax 10 */ 11 12 /** 13 * Define the methods used by AcMenu plugin to produce the plugin's output. 14 * 15 * @package syntax_acmenu 16 */ 17class syntax_plugin_acmenu extends DokuWiki_Syntax_Plugin 18{ 19 20 /** 21 * Define the syntax types that this plugin applies when founds its token. 22 * 23 * @return string 24 */ 25 public function getType() 26 { 27 return "substition"; 28 } 29 30 /** 31 * Define how the plugin's output is handled regarding paragraphs. 32 * 33 * Open paragraphs will be closed before plugin's output: 34 * <p>foo</p> 35 * <acmenu> 36 * <p>bar</p> 37 * 38 * @return string 39 */ 40 public function getPType() 41 { 42 return "block"; 43 } 44 45 /** 46 * Define the priority used to determine in which order modes are 47 * added: the mode with the lowest sort number will win. 48 * 49 * Since this plugin provides internal links, it is sorted at: 50 * AcMenu plugin < Doku_Parser_Mode_internallink (= 300) 51 * 52 * @return integer 53 */ 54 public function getSort() 55 { 56 return 295; 57 } 58 59 /** 60 * Define the regular expression needed to match the plugin's syntax. 61 * 62 * The following general syntax is used: 63 * <acmenu [list of parameters]> 64 * 65 * @param string $mode 66 * name for the format mode of the final output 67 */ 68 public function connectTo($mode) 69 { 70 $this->Lexer->addSpecialPattern("<acmenu.*?>", $mode, "plugin_acmenu"); 71 } 72 73 /** 74 * Handle the plugin's syntax matched. 75 * 76 * This function is called every time the parser encounters the 77 * plugin's syntax in order to produce a list of instructions for the 78 * renderer, which will be interpreted later. 79 * 80 * @param string $match 81 * the text matched 82 * @param integer $state 83 * the lexer state 84 * @param integer $pos 85 * the character position of the matched text 86 * @param object $handler 87 * object reference to the Doku_Handler class 88 * @return array $data 89 * the parameters handled by the plugin's syntax 90 */ 91 public function handle($match, $state, $pos, Doku_Handler $handler) 92 { 93 $data = array(); 94 $match = substr($match, 7, -1); // strips "<acmenu" and ">" 95 96 return $data; 97 } 98 99 /** 100 * Process the list of instructions that render the final output. 101 * 102 * @param string $mode 103 * name for the format mode of the final output 104 * @param object $renderer 105 * object reference to the Doku_Render class 106 * @param array $data 107 * the parameters handled by the plugin's syntax 108 */ 109 public function render($mode, Doku_Renderer $renderer, $data) 110 { 111 global $INFO; 112 global $conf; 113 114 // disable the cache 115 $renderer->nocache(); 116 117 // get the namespace genealogy of the current id 118 // and store it as metadata to be later used in javascript 119 $id = isset($INFO["id"]) ? $INFO["id"] : ""; 120 $sub_ns = $this->_get_sub_ns($id); 121 p_set_metadata($id, array("plugin" => array("plugin_acmenu" => array("sub_ns" => $sub_ns))), false, false); 122 123 // build the namespace tree structure 124 $ns_acmenu = $this->_get_ns_acmenu($sub_ns); // namespace where <acmenu> belongs 125 $level = 0; 126 $tree = $this->_tree($ns_acmenu, $level); 127 $tree = $this->_sort_ns_pg($tree); 128 129 // get heading and id of the namespace where <acmenu> belongs 130 $base_id = implode(":", array_filter(array($ns_acmenu, $conf["start"]))); 131 $base_heading = p_get_first_heading($base_id); 132 if (!isset($base_heading)) { 133 $base_heading = $ns_acmenu; 134 } 135 136 // get cookies 137 $open_items = $this->_get_cookie(); 138 139 // print the namespace tree structure 140 $renderer->doc .= "<div class='acmenu'>"; 141 $renderer->doc .= "<ul class='idx'>"; 142 $renderer->doc .= "<li class='open'>"; 143 $renderer->doc .= "<div class='li'>"; 144 $renderer->doc .= "<span class='curid'>"; 145 $renderer->internallink($base_id, $base_heading); 146 $renderer->doc .= "</span>"; 147 $renderer->doc .= "</div>"; 148 $renderer->doc .= "<ul class='idx'>"; 149 $this->_print($renderer, $tree, $sub_ns, $open_items); 150 $renderer->doc .= "</ul>"; 151 $renderer->doc .= "</li>"; 152 $renderer->doc .= "</ul>"; 153 $renderer->doc .= "</div>"; 154 } 155 156 /** 157 * Get cookies. 158 * 159 * @return array $open_items 160 * the id of the <start> pages to keep open in the form: 161 * array { 162 * [i] => (str) "<ns-acmenu>:<ns-1>:..:<ns-i>:<start>" 163 * } 164 */ 165 private function _get_cookie() 166 { 167 $open_items = array(); 168 if (array_key_exists("plugin_acmenu_open_items", $_COOKIE)) { 169 $open_items = json_decode($_COOKIE["plugin_acmenu_open_items"]); 170 } 171 172 return $open_items; 173 } 174 175 /** 176 * Get the parent namespace where <acmenu> belongs. 177 * 178 * Start from the current namespace (the namespace of the current page) 179 * and go up till the base namespace (the namespace where <acmenu> belongs). 180 * 181 * @param array $sub_ns 182 * the namespace genealogy of the current page id, in the form: 183 * array { 184 * [0] => (str) "<ns-acmenu>:<ns-1>:...:<ns-i>" 185 * ... 186 * [i-1] => (str) "<ns-acmenu>" 187 * [i] => (str) "" 188 * } 189 * @return string $ns_acmenu 190 * the parent namespace where <acmenu> belongs, in the form: 191 * <ns-acmenu> 192 */ 193 private function _get_ns_acmenu($sub_ns) 194 { 195 global $INFO; 196 global $conf; 197 $dir = realpath($conf["savedir"] . "/pages"); 198 $ns_acmenu = ""; 199 200 if (!empty($INFO["namespace"])) { 201 foreach ($sub_ns as $ns) { 202 $sidebar = implode("/", array_filter(array(str_replace(":", "/", $ns), $conf["sidebar"]))); 203 if (file_exists($dir . "/" . $sidebar . ".txt")) { 204 $ns_acmenu = $ns; 205 break; 206 } 207 } 208 } 209 210 return $ns_acmenu; 211 } 212 213 /** 214 * Build the namespace tree structure. 215 * 216 * Start from the base namespace (the namespace where <acmenu> belongs) 217 * and go down until the end. 218 * 219 * @param string $ns_acmenu 220 * the parent namespace where <acmenu> belongs, in the form: 221 * <ns-acmenu> 222 * @param string $level 223 * the indentation level from which start to build the tree structure 224 * @return array $tree 225 * the namespace tree, in the form: 226 * array { 227 * [0] => array { 228 * ["heading"] => (str) "<heading>" 229 * ["id"] => (str) "<id>" 230 * ["level"] => (int) "<level>" 231 * ["type"] => (str) "ns" 232 * ["sub"] => array { 233 * [0] => array { 234 * ["heading"] => (str) "<heading>" 235 * ["id"] => (str) "<id>" 236 * ["level"] => (int) "<level>" 237 * ["type"] => (str) "pg" || "ext_ns" 238 * } 239 * [i] => array {...} 240 * } 241 * } 242 * [i] => array {...} 243 * } 244 * where: 245 * ["type"] = "ns" means "namespace" 246 * ["type"] = "pg" means "page" 247 * ["type"] = "ext_ns" means "external namespace" 248 * so that only namespaces can have ["sub"] namespaces 249 * and external namespaces are treated as pages 250 */ 251 private function _tree($ns_acmenu, $level) 252 { 253 global $conf; 254 $tree = array(); 255 $level = $level + 1; 256 $dir = $conf["savedir"] ."/pages/" . str_replace(":", "/", $ns_acmenu); 257 $files = array_diff(scandir($dir), array("..", ".")); 258 foreach ($files as $file) { 259 if (is_file($dir . "/" . $file)) { 260 $pg_name = cleanID(utf8_decodeFN(substr_replace($file, "", -strlen(".txt")))); 261 $id = implode(":", array_filter(array($ns_acmenu, $pg_name), "strlen")); 262 if (!isHiddenPage($id)) { 263 if (auth_quickaclcheck($id) >= AUTH_READ) { 264 $heading = $pg_name; 265 if (useheading("navigation") && $pg_name != $conf["start"]) { 266 $heading = p_get_first_heading($id); 267 } 268 $tree[] = array("heading" => $heading, 269 "id" => $id, 270 "level" => $level, 271 "type" => "pg"); 272 } 273 } 274 } else { 275 $id = implode(":", array_filter(array($ns_acmenu, $file, $conf["start"]), "strlen")); 276 if ($conf["sneaky_index"] && auth_quickaclcheck($id) < AUTH_READ) { 277 continue; 278 } else { 279 $heading = $file; 280 if (useheading("navigation")) { 281 $heading = p_get_first_heading($id); 282 } 283 if (file_exists($dir . "/" . $file . "/" . $conf["sidebar"] . ".txt")) { 284 // subnamespace with sidebar (external namespace) will not be scanned 285 $tree[] = array("heading" => $heading, 286 "id" => $id, 287 "level" => $level, 288 "type" => "ext_ns"); 289 continue; 290 } else { 291 $tree[] = array("heading" => $heading, 292 "id" => $id, 293 "level" => $level, 294 "type" => "ns", 295 "sub" => $this->_tree(implode(":", array_filter(array($ns_acmenu, $file), "strlen")), $level)); 296 } 297 } 298 } 299 } 300 301 return $tree; 302 } 303 304 /** 305 * Get the namespace genealogy of the given id. 306 * 307 * @param string $id 308 * the current page id, in the form: 309 * <ns-acmenu>:<ns-1>:...:<ns-i>:<pg> 310 * @return array $sub_ns 311 * the namespace genealogy of the current page id, in the form: 312 * array { 313 * [0] => (str) "<ns-acmenu>:<ns-1>:...:<ns-i>" 314 * ... 315 * [i-1] => (str) "<ns-acmenu>" 316 * [i] => (str) "" 317 * } 318 */ 319 private function _get_sub_ns($id) 320 { 321 $sub_ns = array(); 322 $pieces = explode(":", $id); 323 array_pop($pieces); // remove <pg> 324 325 $cp_pieces = $pieces; 326 foreach ($pieces as $key => $val) { 327 $sub_ns[] = implode(":", $cp_pieces); 328 array_pop($cp_pieces); 329 } 330 $sub_ns[] = ""; 331 332 return $sub_ns; 333 } 334 335 /** 336 * Print the namespace tree structure. 337 * 338 * @param object $renderer 339 * object reference to the Doku_Render class 340 * @param array $tree 341 * the namespace tree, in the form: 342 * array { 343 * [0] => array { 344 * ["heading"] => (str) "<heading>" 345 * ["id"] => (str) "<id>" 346 * ["level"] => (int) "<level>" 347 * ["type"] => (str) "ns" 348 * ["sub"] => array { 349 * [0] => array { 350 * ["heading"] => (str) "<heading>" 351 * ["id"] => (str) "<id>" 352 * ["level"] => (int) "<level>" 353 * ["type"] => (str) "pg" || "ext_ns" 354 * } 355 * [i] => array {...} 356 * } 357 * } 358 * [i] => array {...} 359 * } 360 * where: 361 * ["type"] = "ns" means "namespace" 362 * ["type"] = "pg" means "page" 363 * ["type"] = "ext_ns" means "external namespace" 364 * so that only namespaces can have ["sub"] namespaces 365 * and external namespaces are treated as pages 366 * @param array $sub_ns 367 * the namespace genealogy of the current page id, in the form: 368 * array { 369 * [0] => (str) "<ns-acmenu>:<ns-1>:...:<ns-i>" 370 * ... 371 * [i-1] => (str) "<ns-acmenu>" 372 * [i] => (str) "" 373 * } 374 * @param array $open_items 375 * the namespaces to keep open, in the form: 376 * array { 377 * [i] => (str) "<ns_acmenu>:<ns-1>:...:<ns-i>" 378 * } 379 */ 380 private function _print($renderer, $tree, $sub_ns, $open_items) 381 { 382 global $conf; 383 foreach ($tree as $key => $val) { 384 if ($val["type"] == "pg") { 385 $renderer->doc .= "<li class='level" . $val["level"]."'>"; 386 $renderer->doc .= "<div class='li'>"; 387 $renderer->internallink($val["id"], $val["heading"]); 388 $renderer->doc .= "</div>"; 389 $renderer->doc .= "</li>"; 390 } elseif ($val["type"] == "ext_ns") { 391 $renderer->doc .= "<li class='level" . $val["level"]." divert'>"; 392 $renderer->doc .= "<div class='li'>"; 393 $renderer->internallink($val["id"], $val["heading"]); 394 $renderer->doc .= "</div>"; 395 $renderer->doc .= "</li>"; 396 } elseif ($val["type"] == "ns") { 397 if (in_array(substr($val["id"], 0, -strlen(":" . $conf["start"])), $sub_ns) 398 || in_array($val["id"], $open_items)) { 399 $renderer->doc .= "<li class='open'>"; 400 } else { 401 $renderer->doc .= "<li class='closed'>"; 402 } 403 $renderer->doc .= "<div class='li'>"; 404 if (in_array(substr($val["id"], 0, -strlen(":" . $conf["start"])), $sub_ns)) { 405 $renderer->doc .= "<span class='curid'>"; 406 $renderer->internallink($val["id"], $val["heading"]); 407 $renderer->doc .= "</span>"; 408 } else { 409 $renderer->internallink($val["id"], $val["heading"]); 410 } 411 $renderer->doc .= "</div>"; 412 if (in_array(substr($val["id"], 0, -strlen(":" . $conf["start"])), $sub_ns) 413 || in_array($val["id"], $open_items)) { 414 $renderer->doc .= "<ul class='idx'>"; 415 } else { 416 $renderer->doc .= "<ul class='idx' style='display: none'>"; 417 } 418 $this->_print($renderer, $val["sub"], $sub_ns, $open_items); 419 $renderer->doc .= "</ul>"; 420 $renderer->doc .= "</li>"; 421 } 422 } 423 } 424 425 /** 426 * Sort the tree namespace in ascending order. 427 * 428 * The tree is sorted in this order: 429 * 1) namespaces; 430 * 2) pages. 431 * 432 * @param array $tree 433 * the namespace tree, in the form: 434 * array { 435 * [0] => array { 436 * ["heading"] => (str) "<heading>" 437 * ["id"] => (str) "<id>" 438 * ["level"] => (int) "<level>" 439 * ["type"] => (str) "ns" 440 * ["sub"] => array { 441 * [0] => array { 442 * ["heading"] => (str) "<heading>" 443 * ["id"] => (str) "<id>" 444 * ["level"] => (int) "<level>" 445 * ["type"] => (str) "pg" || "ext_ns" 446 * } 447 * [i] => array {...} 448 * } 449 * } 450 * [i] => array {...} 451 * } 452 * where: 453 * ["type"] = "ns" means "namespace" 454 * ["type"] = "pg" means "page" 455 * ["type"] = "ext_ns" means "external namespace" 456 * so that only namespaces can have ["sub"] namespaces 457 * and external namespaces are treated as pages 458 * @return array $tree 459 * the tree namespace sorted 460 */ 461 private function _sort_ns_pg($tree) 462 { 463 global $conf; 464 $ns = array(); 465 $pg = array(); 466 467 foreach ($tree as $key => $val) { 468 if ($val["type"] == "ns") { 469 $val["sub"] = $this->_sort_ns_pg($val["sub"]); 470 $ns[] = $val; 471 } else { 472 $pg[] = $val; 473 } 474 } 475 sort($ns); 476 sort($pg); 477 $tree = array_merge($ns, $pg); 478 foreach ($tree as $key => $array_val) { 479 if ($array_val["heading"] == $conf["start"]) { 480 unset($tree[$key]); 481 array_unshift($tree, $array_val); 482 } 483 } 484 485 return $tree; 486 } 487} 488