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\ExceptionCombo; 10use ComboStrap\SnippetManager; 11use ComboStrap\MarkupRef; 12use ComboStrap\PluginUtility; 13 14if (!defined('DOKU_INC')) die(); 15 16 17class syntax_plugin_combo_minimap extends DokuWiki_Syntax_Plugin 18{ 19 20 const MINIMAP_TAG_NAME = 'minimap'; 21 const INCLUDE_DIRECTORY_PARAMETERS = 'includedirectory'; 22 const SHOW_HEADER = 'showheader'; 23 const NAMESPACE_KEY_ATT = 'namespace'; 24 25 26 function connectTo($aMode) 27 { 28 $pattern = '<' . self::MINIMAP_TAG_NAME . '[^>]*>'; 29 $this->Lexer->addSpecialPattern($pattern, $aMode, PluginUtility::getModeFromTag($this->getPluginComponent())); 30 } 31 32 function getSort() 33 { 34 /** 35 * One less than the old one 36 */ 37 return 149; 38 } 39 40 /** 41 * No p element please 42 * @return string 43 */ 44 function getPType() 45 { 46 return 'block'; 47 } 48 49 function getType() 50 { 51 // The spelling is wrong but this is a correct value 52 // https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 53 return 'substition'; 54 } 55 56 /** 57 * 58 * The handle function goal is to parse the matched syntax through the pattern function 59 * and to return the result for use in the renderer 60 * This result is always cached until the page is modified. 61 * @param string $match 62 * @param int $state 63 * @param int $pos 64 * @param Doku_Handler $handler 65 * @return array|bool 66 * @see DokuWiki_Syntax_Plugin::handle() 67 * 68 */ 69 function handle($match, $state, $pos, Doku_Handler $handler) 70 { 71 72 switch ($state) { 73 74 // As there is only one call to connect to in order to a add a pattern, 75 // there is only one state entering the function 76 // but I leave it for better understanding of the process flow 77 case DOKU_LEXER_SPECIAL : 78 79 // Parse the parameters 80 $match = substr($match, 8, -1); //9 = strlen("<minimap") 81 82 // Init 83 $parameters = array(); 84 $parameters['substr'] = 1; 85 $parameters[self::INCLUDE_DIRECTORY_PARAMETERS] = $this->getConf(self::INCLUDE_DIRECTORY_PARAMETERS); 86 $parameters[self::SHOW_HEADER] = $this->getConf(self::SHOW_HEADER); 87 88 89 // /i not case sensitive 90 $attributePattern = "\\s*(\w+)\\s*=\\s*[\'\"]{1}([^\`\"]*)[\'\"]{1}\\s*"; 91 $result = preg_match_all('/' . $attributePattern . '/i', $match, $matches); 92 if ($result != 0) { 93 foreach ($matches[1] as $key => $parameterKey) { 94 $parameter = strtolower($parameterKey); 95 $value = $matches[2][$key]; 96 if (in_array($parameter, [self::SHOW_HEADER, self::INCLUDE_DIRECTORY_PARAMETERS])) { 97 $value = filter_var($value, FILTER_VALIDATE_BOOLEAN); 98 } 99 $parameters[$parameter] = $value; 100 } 101 } 102 // Cache the values 103 return array( 104 PluginUtility::STATE => $state, 105 PluginUtility::ATTRIBUTES => $parameters 106 ); 107 108 } 109 110 return false; 111 } 112 113 114 function render($mode, Doku_Renderer $renderer, $data) 115 { 116 117 // The $data variable comes from the handle() function 118 // 119 // $mode = 'xhtml' means that we output html 120 // There is other mode such as metadata where you can output data for the headers (Not 100% sure) 121 if ($mode == 'xhtml') { 122 123 /** @var Doku_Renderer_xhtml $renderer */ 124 125 126 $state = $data[PluginUtility::STATE]; 127 128 // As there is only one call to connect to in order to a add a pattern, 129 // there is only one state entering the function 130 // but I leave it for better understanding of the process flow 131 switch ($state) { 132 133 case DOKU_LEXER_SPECIAL : 134 135 136 PluginUtility::getSnippetManager()->attachCssInternalStyleSheetForSlot(self::MINIMAP_TAG_NAME); 137 138 139 global $ID; 140 global $INFO; 141 $callingId = $ID; 142 // If mini-map is in a sidebar, we don't want the ID of the sidebar 143 // but the ID of the page. 144 if ($INFO != null) { 145 $callingId = $INFO['id']; 146 } 147 148 $attributes = $data[PluginUtility::ATTRIBUTES]; 149 $nameSpacePath = getNS($callingId); // The complete path to the directory 150 if (array_key_exists(self::NAMESPACE_KEY_ATT, $attributes)) { 151 $nameSpacePath = $attributes[self::NAMESPACE_KEY_ATT]; 152 } 153 $currentNameSpace = curNS($callingId); // The name of the container directory 154 $includeDirectory = $attributes[self::INCLUDE_DIRECTORY_PARAMETERS]; 155 $pagesOfNamespace = $this->getNamespaceChildren($nameSpacePath, $sort = 'natural', $listdirs = $includeDirectory); 156 157 // Set the two possible home page for the namespace ie: 158 // - the name of the containing map ($homePageWithContainingMapName) 159 // - the start conf parameters ($homePageWithStartConf) 160 global $conf; 161 $parts = explode(':', $nameSpacePath); 162 $lastContainingNameSpace = $parts[count($parts) - 1]; 163 $homePageWithContainingMapName = $nameSpacePath . ':' . $lastContainingNameSpace; 164 $startConf = $conf['start']; 165 $homePageWithStartConf = $nameSpacePath . ':' . $startConf; 166 167 // Build the list of page 168 $miniMapList = '<ul class="list-group">'; 169 $pageNum = 0; 170 $startPageFound = false; 171 $homePageFound = false; 172 //$pagesCount = count($pagesOfNamespace); // number of pages in the namespace 173 foreach ($pagesOfNamespace as $pageArray) { 174 175 // The title of the page 176 $title = ''; 177 178 // If it's a directory 179 if ($pageArray['type'] == "d") { 180 181 $pageId = $this->getNamespaceStartId($pageArray['id']); 182 183 } else { 184 185 $pageNum++; 186 $pageId = $pageArray['id']; 187 188 } 189 $markupRef = new MarkupRef($pageId); 190 191 192 193 /** 194 * Label 195 */ 196 $label = $markupRef->getLabel(); 197 // Suppress the parts in the name with the regexp defines in the 'suppress' params 198 if ($attributes['suppress']) { 199 $substrPattern = '/' . $attributes['suppress'] . '/i'; 200 $replacement = ''; 201 $label = preg_replace($substrPattern, $replacement, $label); 202 } 203 // If debug mode 204 if ($attributes['debug']) { 205 $label .= ' (' . $pageId . '|' . $pageNum . ')'; 206 } 207 208 /** 209 * Link attributes 210 */ 211 try { 212 $linkAttribute = $markupRef->toAttributes(); 213 } catch (ExceptionCombo $e) { 214 $miniMapList .= \ComboStrap\LogUtility::wrapInRedForHtml("Error. {$e->getMessage()}"); 215 continue; 216 } 217 // See in which page we are 218 // The style will then change 219 $active = ''; 220 if ($callingId == $pageId) { 221 $linkAttribute->addEmptyOutputAttributeValue('active'); 222 } 223 224 // Not all page are printed 225 // sidebar are not for instance 226 227 // Are we in the root ? 228 if ($pageArray['ns']) { 229 $nameSpacePathPrefix = $pageArray['ns'] . ':'; 230 } else { 231 $nameSpacePathPrefix = ''; 232 } 233 $print = true; 234 if ($pageArray['id'] == $nameSpacePathPrefix . $currentNameSpace) { 235 // If the start page exists, the page with the same name 236 // than the namespace must be shown 237 if (page_exists($nameSpacePathPrefix . $startConf)) { 238 $print = true; 239 } else { 240 $print = false; 241 } 242 $homePageFound = true; 243 } else if ($pageArray['id'] == $nameSpacePathPrefix . $startConf) { 244 $print = false; 245 $startPageFound = true; 246 } else if ($pageArray['id'] == $nameSpacePathPrefix . $conf['sidebar']) { 247 $pageNum -= 1; 248 $print = false; 249 }; 250 251 252 // If the page must be printed, build the link 253 if ($print) { 254 255 // Open the item tag 256 $miniMapList .= "<li class=\"list-group-item " . $active . "\">"; 257 258 // Add a glyphicon if it's a directory 259 if ($pageArray['type'] == "d") { 260 $miniMapList .= "<span class=\"nicon_folder_open\" aria-hidden=\"true\"></span> "; 261 } 262 263 $miniMapList .= $linkAttribute->toHtmlEnterTag("a"); 264 $miniMapList .= $label; 265 $miniMapList .= "</a>"; 266 267 268 // Close the item 269 $miniMapList .= "</li>"; 270 271 } 272 273 } 274 $miniMapList .= '</ul>'; // End list-group 275 276 277 // Build the panel header 278 $miniMapHeader = ""; 279 $startId = ""; 280 if ($startPageFound) { 281 $startId = $homePageWithStartConf; 282 } else { 283 if ($homePageFound) { 284 $startId = $homePageWithContainingMapName; 285 } 286 } 287 288 $panelHeaderContent = ""; 289 if ($startId == "") { 290 if ($attributes[self::SHOW_HEADER] == true) { 291 $panelHeaderContent = 'No Home Page found'; 292 } 293 } else { 294 $startLink = new MarkupRef($startId); 295 try { 296 $panelHeaderContent = $startLink->toAttributes()->toHtmlEnterTag("a"); 297 $panelHeaderContent .= $startLink->getLabel(); 298 $panelHeaderContent .= "</a>"; 299 } catch (ExceptionCombo $e) { 300 $panelHeaderContent = "Error: {$e->getMessage()}"; 301 } 302 // We are not counting the header page 303 $pageNum--; 304 } 305 306 if ($panelHeaderContent != "") { 307 $miniMapHeader .= '<div class="panel-heading">' . $panelHeaderContent . ' <span class="label label-primary">' . $pageNum . ' pages</span></div>'; 308 } 309 310 if ($attributes['debug']) { 311 $miniMapHeader .= '<div class="panel-body">' . 312 '<B>Debug Information:</B><BR>' . 313 'CallingId: (' . $callingId . ')<BR>' . 314 'Suppress Option: (' . $attributes['suppress'] . ')<BR>' . 315 '</div>'; 316 } 317 318 // Header + list 319 $renderer->doc .= '<div id="minimap__plugin"><div class="panel panel-default">' 320 . $miniMapHeader 321 . $miniMapList 322 . '</div></div>'; 323 break; 324 } 325 326 return true; 327 } 328 return false; 329 330 } 331 332 /** 333 * Return all pages and/of sub-namespaces (subdirectory) of a namespace (ie directory) 334 * Adapted from feed.php 335 * 336 * @param $namespace The container of the pages 337 * @param string $sort 'natural' to use natural order sorting (default); 'date' to sort by filemtime 338 * @param $listdirs - Add the directory to the list of files 339 * @return array An array of the pages for the namespace 340 */ 341 function getNamespaceChildren($namespace, $sort = 'natural', $listdirs = false) 342 { 343 require_once(DOKU_INC . 'inc/search.php'); 344 global $conf; 345 346 $ns = ':' . cleanID($namespace); 347 // ns as a path 348 $ns = utf8_encodeFN(str_replace(':', '/', $ns)); 349 350 $data = array(); 351 352 // Options of the callback function search_universal 353 // in the search.php file 354 $search_opts = array( 355 'depth' => 1, 356 'pagesonly' => true, 357 'listfiles' => true, 358 'listdirs' => $listdirs, 359 'firsthead' => true 360 ); 361 // search_universal is a function in inc/search.php that accepts the $search_opts parameters 362 search($data, $conf['datadir'], 'search_universal', $search_opts, $ns, $lvl = 1, $sort); 363 364 return $data; 365 } 366 367 /** 368 * Return the id of the start page of a namespace 369 * 370 * @param $id an id of a namespace (directory) 371 * @return string the id of the home page 372 */ 373 function getNamespaceStartId($id) 374 { 375 376 global $conf; 377 378 $id = $id . ":"; 379 380 if (page_exists($id . $conf['start'])) { 381 // start page inside namespace 382 $homePageId = $id . $conf['start']; 383 } elseif (page_exists($id . noNS(cleanID($id)))) { 384 // page named like the NS inside the NS 385 $homePageId = $id . noNS(cleanID($id)); 386 } elseif (page_exists($id)) { 387 // page like namespace exists 388 $homePageId = substr($id, 0, -1); 389 } else { 390 // fall back to default 391 $homePageId = $id . $conf['start']; 392 } 393 return $homePageId; 394 } 395 396 /** 397 * @param $get_called_class 398 * @return string 399 */ 400 public static function getTagName($get_called_class) 401 { 402 list(/* $t */, /* $p */, $c) = explode('_', $get_called_class, 3); 403 return (isset($c) ? $c : ''); 404 } 405 406 /** 407 * @return string - the tag 408 */ 409 public static function getTag() 410 { 411 return self::getTagName(get_called_class()); 412 } 413 414 415} 416