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