1<?php 2/** 3 * Plugin minimap : Displays mini-map for namespace 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Nicolas GERARD 7 */ 8 9use ComboStrap\LinkUtility; 10use ComboStrap\PluginUtility; 11 12if (!defined('DOKU_INC')) die(); 13 14 15class syntax_plugin_combo_minimap extends DokuWiki_Syntax_Plugin 16{ 17 18 const MINIMAP_TAG_NAME = 'minimap'; 19 const INCLUDE_DIRECTORY_PARAMETERS = 'includedirectory'; 20 const SHOW_HEADER = 'showheader'; 21 const NAMESPACE_KEY_ATT = 'namespace'; 22 const POWERED_BY = 'poweredby'; 23 24 const STYLE_SNIPPET = <<<EOF 25<style> 26.nicon_folder_open { 27 background-image: url('data:image/svg+xml;charset=utf8,<svg xmlns="http://www.w3.org/2000/svg" width="250" height="195"><g fill="rgb(204,204,204)" transform="translate(-7.897 -268.6)"><rect rx="0" y="286.829" x="12.897" height="175" width="200" opacity=".517"/><path d="M13.23 458.808l39.687-132.291h198.437l-39.687 132.291z" fill-rule="evenodd"/><rect rx="0" y="273.6" x="39.688" height="13" width="90"/></g></svg>'); 28 display: inline-block; 29 width: 1.5em; 30 height: 1em; 31 vertical-align: middle; 32 content: ""; 33 background-size: 100% 100%; 34} 35#minimap__plugin { 36 font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 37 font-size: 14px; 38 line-height: 1.42857; 39} 40 41#minimap__plugin .panel-default { 42 border-color: #ddd; 43 box-sizing: border-box; 44} 45 46#minimap__plugin .panel { 47 box-sizing: border-box; 48 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 49 -moz-border-bottom-colors: none; 50 -moz-border-left-colors: none; 51 -moz-border-right-colors: none; 52 -moz-border-top-colors: none; 53 background-color: #fff; 54 border-image-outset: 0 0 0 0; 55 border-image-repeat: stretch stretch; 56 border-image-slice: 100% 100% 100% 100%; 57 border-image-source: none; 58 border-image-width: 1 1 1 1; 59 border-radius: 4px; 60 border: 1px solid; 61 margin-bottom: 20px; 62 display: block; 63 color: #ddd; 64} 65 66#minimap__plugin .panel-default > .panel-heading { 67 background: #f5f5f5 linear-gradient(to bottom, #f5f5f5 0px, #e8e8e8 100%) repeat-x; 68 border-color: #ddd; 69 color: #333; 70} 71 72#minimap__plugin .panel-heading { 73 border-bottom: 1px solid; 74 border-top-left-radius: 3px; 75 border-top-right-radius: 3px; 76 padding: 10px 15px; 77 box-sizing: border-box; 78 display: block; 79 font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 80 font-size: 14px; 81 line-height: 1.42857; 82} 83 84#minimap__plugin .panel > .list-group, #minimap__plugin .panel > .panel-collapse > .list-group { 85 margin-bottom: 0; 86} 87 88#minimap__plugin .list-group { 89 border-radius: 4px; 90 box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075); 91 padding-left: 0; 92 box-sizing: border-box; 93 color: #333; 94} 95 96#minimap__plugin .panel-heading + .list-group .list-group-item:first-child { 97 border-top-width: 0; 98} 99 100#minimap__plugin .panel > .list-group .list-group-item, 101#minimap__plugin .panel > .panel-collapse > .list-group .list-group-item { 102 border-bottom-width: 1px; 103 border-left-width: 0; 104 border-right-width: 0; 105 border-radius: 0; 106} 107 108#minimap__plugin .list-group-item { 109 -moz-border-bottom-colors: none; 110 -moz-border-left-colors: none; 111 -moz-border-right-colors: none; 112 -moz-border-top-colors: none; 113 background-color: #fff; 114 border-image-outset: 0 0 0 0; 115 border-image-repeat: stretch stretch; 116 border-image-slice: 100% 100% 100% 100%; 117 border-image-source: none; 118 border-image-width: 1 1 1 1; 119 /*border: solid #ddd;*/ 120 display: block; 121 padding: 10px 15px; 122 position: relative; 123 box-sizing: border-box; 124 margin: 0 0 -1px; 125} 126 127#minimap__plugin .label-primary { 128 background-color: #337ab7; 129} 130 131#minimap__plugin .label { 132 border-radius: 0.25em; 133 color: #fff; 134 display: inline; 135 font-size: 75%; 136 font-weight: 700; 137 line-height: 1; 138 padding: 0.2em 0.6em 0.3em; 139 text-align: center; 140 vertical-align: baseline; 141 white-space: nowrap; 142 box-sizing: border-box; 143} 144 145/* Active link css */ 146#minimap__plugin .list-group-item.active, 147#minimap__plugin .list-group-item.active:focus, 148#minimap__plugin .list-group-item.active:hover { 149 background: #f5f5f5 linear-gradient(to bottom, #f5f5f5 0px, #e8e8e8 100%) repeat-x; 150 border-color: #ddd; 151 color: #333; 152 text-shadow: none; 153} 154 155#minimap__plugin .list-group-item.active { 156 background-color: #e8e8e8 ! important; 157 z-index: 2; 158} 159 160 161#minimap__plugin .panel-body { 162 clear: both; 163 content: " "; 164 box-sizing: border-box; 165 display: table; 166 padding: 15px; 167 unicode-bidi: -moz-isolate; 168 color: #333; 169} 170 171#minimap__plugin .glyphicon { 172 /*already same color than the header*/ 173 color: #d8d2d2; 174} 175 176#minimap__plugin .panel-footing { 177 display: flex; 178 padding: 0.10rem; 179 background: #f5f5f5 linear-gradient(to bottom,#f5f5f5 0px,#e8e8e8 100%) repeat-x; 180} 181 182#minimap__plugin .minimap_badge { 183 184 border-radius: 0.25em; 185 background-color: #d8d2d2; 186 font-size: 0.6rem; 187 padding: 0.25rem; 188 margin-left: auto !important; 189 margin-right: 0.3rem; 190 color: #1d4a71; 191 margin: 0.1rem; 192} 193</style> 194EOF; 195 196 197 function connectTo($aMode) 198 { 199 $pattern = '<' . self::MINIMAP_TAG_NAME . '[^>]*>'; 200 $this->Lexer->addSpecialPattern($pattern, $aMode, PluginUtility::getModeForComponent($this->getPluginComponent())); 201 } 202 203 function getSort() 204 { 205 /** 206 * One less than the old one 207 */ 208 return 149; 209 } 210 211 /** 212 * No p element please 213 * @return string 214 */ 215 function getPType() 216 { 217 return 'block'; 218 } 219 220 function getType() 221 { 222 // The spelling is wrong but this is a correct value 223 // https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 224 return 'substition'; 225 } 226 227 /** 228 * 229 * The handle function goal is to parse the matched syntax through the pattern function 230 * and to return the result for use in the renderer 231 * This result is always cached until the page is modified. 232 * @param string $match 233 * @param int $state 234 * @param int $pos 235 * @param Doku_Handler $handler 236 * @return array|bool 237 * @see DokuWiki_Syntax_Plugin::handle() 238 * 239 */ 240 function handle($match, $state, $pos, Doku_Handler $handler) 241 { 242 243 switch ($state) { 244 245 // As there is only one call to connect to in order to a add a pattern, 246 // there is only one state entering the function 247 // but I leave it for better understanding of the process flow 248 case DOKU_LEXER_SPECIAL : 249 250 // Parse the parameters 251 $match = substr($match, 8, -1); //9 = strlen("<minimap") 252 253 // Init 254 $parameters = array(); 255 $parameters['substr'] = 1; 256 $parameters[self::INCLUDE_DIRECTORY_PARAMETERS] = $this->getConf(self::INCLUDE_DIRECTORY_PARAMETERS); 257 $parameters[self::SHOW_HEADER] = $this->getConf(self::SHOW_HEADER); 258 259 260 // /i not case sensitive 261 $attributePattern = "\\s*(\w+)\\s*=\\s*[\'\"]{1}([^\`\"]*)[\'\"]{1}\\s*"; 262 $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches); 263 if ($result != 0) { 264 foreach ($matches[1] as $key => $parameterKey) { 265 $parameter = strtolower($parameterKey); 266 $value = $matches[2][$key]; 267 if (in_array($parameter, [self::SHOW_HEADER, self::INCLUDE_DIRECTORY_PARAMETERS])) { 268 $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); 269 } 270 $parameters[$parameter] = $value; 271 } 272 } 273 // Cache the values 274 return array($state, $parameters); 275 276 } 277 278 return false; 279 } 280 281 282 function render($mode, Doku_Renderer $renderer, $data) 283 { 284 285 // The $data variable comes from the handle() function 286 // 287 // $mode = 'xhtml' means that we output html 288 // There is other mode such as metadata where you can output data for the headers (Not 100% sure) 289 if ($mode == 'xhtml') { 290 291 /** @var Doku_Renderer_xhtml $renderer */ 292 293 // Unfold the $data array in two separates variables 294 list($state, $parameters) = $data; 295 296 // As there is only one call to connect to in order to a add a pattern, 297 // there is only one state entering the function 298 // but I leave it for better understanding of the process flow 299 switch ($state) { 300 301 case DOKU_LEXER_SPECIAL : 302 303 if (!PluginUtility::htmlSnippetAlreadyAdded($renderer->info,self::MINIMAP_TAG_NAME)){ 304 $renderer->doc .= self::STYLE_SNIPPET; 305 }; 306 307 global $ID; 308 global $INFO; 309 $callingId = $ID; 310 // If mini-map is in a sidebar, we don't want the ID of the sidebar 311 // but the ID of the page. 312 if ($INFO != null) { 313 $callingId = $INFO['id']; 314 } 315 316 $nameSpacePath = getNS($callingId); // The complete path to the directory 317 if (array_key_exists(self::NAMESPACE_KEY_ATT, $parameters)) { 318 $nameSpacePath = $parameters[self::NAMESPACE_KEY_ATT]; 319 } 320 $currentNameSpace = curNS($callingId); // The name of the container directory 321 $includeDirectory = $parameters[self::INCLUDE_DIRECTORY_PARAMETERS]; 322 $pagesOfNamespace = $this->getNamespaceChildren($nameSpacePath, $sort = 'natural', $listdirs = $includeDirectory); 323 324 // Set the two possible home page for the namespace ie: 325 // - the name of the containing map ($homePageWithContainingMapName) 326 // - the start conf parameters ($homePageWithStartConf) 327 global $conf; 328 $parts = explode(':', $nameSpacePath); 329 $lastContainingNameSpace = $parts[count($parts) - 1]; 330 $homePageWithContainingMapName = $nameSpacePath . ':' . $lastContainingNameSpace; 331 $startConf = $conf['start']; 332 $homePageWithStartConf = $nameSpacePath . ':' . $startConf; 333 334 // Build the list of page 335 $miniMapList = '<ul class="list-group">'; 336 $pageNum = 0; 337 $startPageFound = false; 338 $homePageFound = false; 339 //$pagesCount = count($pagesOfNamespace); // number of pages in the namespace 340 foreach ($pagesOfNamespace as $pageArray) { 341 342 // The title of the page 343 $title = ''; 344 345 // If it's a directory 346 if ($pageArray['type'] == "d") { 347 348 $pageId = $this->getNamespaceStartId($pageArray['id']); 349 350 } else { 351 352 $pageNum++; 353 $pageId = $pageArray['id']; 354 355 } 356 $link = new LinkUtility($pageId); 357 358 359 /** 360 * Set name and title 361 */ 362 // Name if the variable that it's shown. A part of it can be suppressed 363 // Title will stay full in the link 364 $h1TargetPage = $link->getInternalPage()->getH1(); 365 $title = $link->getInternalPage()->getTitle(); 366 367 $link->setName(noNSorNS($pageId)); 368 if ($h1TargetPage !=null) { 369 $link->setName($h1TargetPage); 370 } else { 371 if ($title!=null) { 372 $link->setName($title); 373 } 374 } 375 $link->setTitle(noNSorNS($pageId)); 376 if ($title!=null) { 377 $link->setTitle($title); 378 } 379 380 // If debug mode 381 if ($parameters['debug']) { 382 $link->setTitle($link->getTitle().' (' . $pageId . ')'); 383 } 384 385 // Add the page number in the URL title 386 $link->setTitle($link->getTitle() .' (' . $pageNum . ')'); 387 388 // Suppress the parts in the name with the regexp defines in the 'suppress' params 389 if ($parameters['suppress']) { 390 $substrPattern = '/' . $parameters['suppress'] . '/i'; 391 $replacement = ''; 392 $name = preg_replace($substrPattern, $replacement, $link->getName()); 393 $link->setName($name); 394 } 395 396 // See in which page we are 397 // The style will then change 398 $active = ''; 399 if ($callingId == $pageId) { 400 $active = 'active'; 401 } 402 403 // Not all page are printed 404 // sidebar are not for instance 405 406 // Are we in the root ? 407 if ($pageArray['ns']) { 408 $nameSpacePathPrefix = $pageArray['ns'] . ':'; 409 } else { 410 $nameSpacePathPrefix = ''; 411 } 412 $print = true; 413 if ($pageArray['id'] == $nameSpacePathPrefix . $currentNameSpace) { 414 // If the start page exists, the page with the same name 415 // than the namespace must be shown 416 if (page_exists($nameSpacePathPrefix . $startConf)) { 417 $print = true; 418 } else { 419 $print = false; 420 } 421 $homePageFound = true; 422 } else if ($pageArray['id'] == $nameSpacePathPrefix . $startConf) { 423 $print = false; 424 $startPageFound = true; 425 } else if ($pageArray['id'] == $nameSpacePathPrefix . $conf['sidebar']) { 426 $pageNum -= 1; 427 $print = false; 428 }; 429 430 431 // If the page must be printed, build the link 432 if ($print) { 433 434 // Open the item tag 435 $miniMapList .= "<li class=\"list-group-item " . $active . "\">"; 436 437 // Add a glyphicon if it's a directory 438 if ($pageArray['type'] == "d") { 439 $miniMapList .= "<span class=\"nicon_folder_open\" aria-hidden=\"true\"></span> "; 440 } 441 442 $miniMapList .= $link->render($renderer);; 443 444 445 // Close the item 446 $miniMapList .= "</li>"; 447 448 } 449 450 } 451 $miniMapList .= '</ul>'; // End list-group 452 453 454 // Build the panel header 455 $miniMapHeader = ""; 456 $startId = ""; 457 if ($startPageFound) { 458 $startId = $homePageWithStartConf; 459 } else { 460 if ($homePageFound) { 461 $startId = $homePageWithContainingMapName; 462 } 463 } 464 465 $panelHeaderContent = ""; 466 if ($startId == "") { 467 if ($parameters[self::SHOW_HEADER] == true) { 468 $panelHeaderContent = 'No Home Page found'; 469 } 470 } else { 471 $startLink = new LinkUtility($startId); 472 $startLink->setTitle($startId); 473 474 $panelHeaderContent = $startLink->render($renderer); 475 // We are not counting the header page 476 $pageNum--; 477 } 478 479 if ($panelHeaderContent != "") { 480 $miniMapHeader .= '<div class="panel-heading">' . $panelHeaderContent . ' <span class="label label-primary">' . $pageNum . ' pages</span></div>'; 481 } 482 483 if ($parameters['debug']) { 484 $miniMapHeader .= '<div class="panel-body">' . 485 '<B>Debug Information:</B><BR>' . 486 'CallingId: (' . $callingId . ')<BR>' . 487 'Suppress Option: (' . $parameters['suppress'] . ')<BR>' . 488 '</div>'; 489 } 490 491 // Header + list 492 $renderer->doc .= '<div id="minimap__plugin"><div class="panel panel-default">' 493 . $miniMapHeader 494 . $miniMapList 495 . '</div></div>'; 496 break; 497 } 498 499 return true; 500 } 501 return false; 502 503 } 504 505 /** 506 * Return all pages and/of sub-namespaces (subdirectory) of a namespace (ie directory) 507 * Adapted from feed.php 508 * 509 * @param $namespace The container of the pages 510 * @param string $sort 'natural' to use natural order sorting (default); 'date' to sort by filemtime 511 * @param $listdirs - Add the directory to the list of files 512 * @return array An array of the pages for the namespace 513 */ 514 function getNamespaceChildren($namespace, $sort = 'natural', $listdirs = false) 515 { 516 require_once(DOKU_INC . 'inc/search.php'); 517 global $conf; 518 519 $ns = ':' . cleanID($namespace); 520 // ns as a path 521 $ns = utf8_encodeFN(str_replace(':', '/', $ns)); 522 523 $data = array(); 524 525 // Options of the callback function search_universal 526 // in the search.php file 527 $search_opts = array( 528 'depth' => 1, 529 'pagesonly' => true, 530 'listfiles' => true, 531 'listdirs' => $listdirs, 532 'firsthead' => true 533 ); 534 // search_universal is a function in inc/search.php that accepts the $search_opts parameters 535 search($data, $conf['datadir'], 'search_universal', $search_opts, $ns, $lvl = 1, $sort); 536 537 return $data; 538 } 539 540 /** 541 * Return the id of the start page of a namespace 542 * 543 * @param $id an id of a namespace (directory) 544 * @return string the id of the home page 545 */ 546 function getNamespaceStartId($id) 547 { 548 549 global $conf; 550 551 $id = $id . ":"; 552 553 if (page_exists($id . $conf['start'])) { 554 // start page inside namespace 555 $homePageId = $id . $conf['start']; 556 } elseif (page_exists($id . noNS(cleanID($id)))) { 557 // page named like the NS inside the NS 558 $homePageId = $id . noNS(cleanID($id)); 559 } elseif (page_exists($id)) { 560 // page like namespace exists 561 $homePageId = substr($id, 0, -1); 562 } else { 563 // fall back to default 564 $homePageId = $id . $conf['start']; 565 } 566 return $homePageId; 567 } 568 569 /** 570 * @param $get_called_class 571 * @return string 572 */ 573 public static function getTagName($get_called_class) 574 { 575 list(/* $t */, /* $p */, $c) = explode('_', $get_called_class, 3); 576 return (isset($c) ? $c : ''); 577 } 578 579 /** 580 * @return string - the tag 581 */ 582 public static function getTag() 583 { 584 return self::getTagName(get_called_class()); 585 } 586 587 588} 589