1<?php 2 3namespace dokuwiki\template\mikio; 4 5/** 6 * DokuWiki Mikio Template 7 * 8 * @link http://dokuwiki.org/template:mikio 9 * @author James Collins <james.collins@outlook.com.au> 10 * @license MIT License (https://raw.githubusercontent.com/nomadjimbob/Mikio/master/LICENSE) 11 */ 12 13if (!defined('DOKU_INC')) die(); 14 15require_once('inc/simple_html_dom.php'); 16 17class Template { 18 public $tplDir = ''; 19 public $baseDir = ''; 20 21 22 /** 23 * Class constructor 24 * 25 * @author James Collins <james.collins@outlook.com.au> 26 */ 27 public function __construct() { 28 $this->tplDir = tpl_incdir(); 29 $this->baseDir = tpl_basedir(); 30 31 $this->_registerHooks(); 32 } 33 34 35 /** 36 * Get the singleton instance 37 * 38 * @return Template 39 */ 40 public static function getInstance() 41 { 42 43 static $instance = null; 44 45 if ($instance === null) { 46 $instance = new Template(); 47 } 48 49 return $instance; 50 51 } 52 53 /** 54 * Register themes DokuWiki hooks 55 * 56 * @author James Collins <james.collins@outlook.com.au> 57 */ 58 private function _registerHooks() { 59 global $EVENT_HANDLER; 60 61 $events_dispatcher = array( 62 'TPL_METAHEADER_OUTPUT' => 'metaheadersHandler', 63 // 'TPL_CONTENT_DISPLAY' => 'contentHandler', 64 ); 65 66 foreach ($events_dispatcher as $event => $method) { 67 $EVENT_HANDLER->register_hook($event, 'BEFORE', $this, $method); 68 } 69 } 70 71 72 /** 73 * DokuWiki META Header event handler 74 * 75 * @author James Collins <james.collins@outlook.com.au> 76 */ 77 public function metaHeadersHandler(\Doku_Event $event) { 78 $stylesheets = array(); 79 $scripts = array(); 80 81 if($this->getConf('useTheme') != '') { 82 if(file_exists($this->tplDir . 'themes/' . $this->getConf('useTheme') . '/style.css')) { 83 $stylesheets[] = $this->baseDir . 'themes/' . $this->getConf('useTheme') . '/style.css'; 84 } 85 if(file_exists($this->tplDir . 'themes/' . $this->getConf('useTheme') . '/script.js')) { 86 $scripts[] = $this->baseDir . 'themes/' . $this->getConf('useTheme') . '/script.js'; 87 } 88 } 89 90 if($this->getConf('includeFontAwesome') == true) $stylesheets[] = $this->baseDir . 'assets/fontawesome/css/all.min.css'; 91 92 $scripts[] = $this->baseDir . 'assets/bootstrap/popper.min.js'; 93 $scripts[] = $this->baseDir . 'assets/bootstrap/bootstrap.min.js'; 94 $stylesheets[] = $this->baseDir . 'assets/bootstrap/bootstrap.min.css'; 95 $stylesheets[] = $this->baseDir . 'assets/mikio.css'; 96 97 foreach ($stylesheets as $style) { 98 array_unshift($event->data['link'], array( 99 'type' => 'text/css', 100 'rel' => 'stylesheet', 101 'href' => $style 102 )); 103 } 104 105 foreach ($scripts as $script) { 106 $event->data['script'][] = array( 107 'type' => 'text/javascript', 108 '_data' => '', 109 'src' => $script 110 ); 111 } 112 } 113 114 115 /** 116 * DokuWiki content event handler 117 * 118 * @author James Collins <james.collins@outlook.com.au> 119 */ 120 public function contentHandler(\Doku_Event $event) 121 { 122 $event->data = $this->parseContent($event->data); 123 } 124 125 126 /** 127 * Parse configuration options 128 * 129 * @author James Collins <james.collins@outlook.com.au> 130 * 131 * @param string $key The configuration key to retreive 132 * @param mixed $default If key doesn't exist, return this value 133 * @return mixed Parsed value of configuration 134 */ 135 public function getConf($key, $default = false) { 136 global $ACT, $conf; 137 138 $value = tpl_getConf($key, $default); 139 140 switch($key) { 141 142 case 'navbar': // TODO is this needed? 143 $value = explode(',', $value); 144 break; 145 146 case 'showSidebar': 147 if ($ACT !== 'show') { 148 return false; 149 } 150 151 return page_findnearest($conf['sidebar'], $this->getConf('useACL')); 152 153 case 'navbarMenuStyle': 154 if($value != 'text') { 155 if(!$this->getConf('useFontAwesome')) { 156 return 'text'; 157 } 158 } 159 160 break; 161 162 case 'navbarMenuPosition': 163 if($value == 'right') { 164 return 'ml-md-auto'; 165 } 166 167 return ''; 168 169 case 'breadcrumbsLoc': 170 if(!$this->getConf('useHeroTitle') && $value == 'hero') { 171 return 'top'; 172 } 173 174 if($value != 'top' && $value != 'hero' && $value != 'page') { 175 return 'page'; 176 } 177 178 break; 179 } 180 181 return $value; 182 } 183 184 185 /** 186 * Icon 187 * 188 * @author James Collins <james.collins@outlook.com.au> 189 * 190 * @param string $type The type of icon to return 191 * @return string HTML for icon element 192 */ 193 public function icon($type) { 194 if($this->getConf('useFontAwesome')) { 195 return '<i class="fa fa-' . $type . '" aria-hidden="true"></i>'; 196 } 197 198 return ''; 199 } 200 201 202 /** 203 * Print the Navbar menu title/icon 204 * 205 * @author James Collins <james.collins@outlook.com.au> 206 * 207 * @param string $type The type of icon to return 208 * @return string HTML for icon element 209 */ 210 public function navbarMenuTitle($title, $icon) { 211 global $lang; 212 213 $menu = ''; 214 215 if($this->getConf('navbarMenuStyle') != 'text') { 216 $menu .= $this->icon($icon); 217 } 218 219 if($this->getConf('navbarMenuStyle') != 'icon') { 220 $menu .= $lang[$title]; 221 } 222 223 echo $menu; 224 } 225 226 227 /** 228 * Add class to first DOM element 229 * 230 * @author James Collins <james.collins@outlook.com.au> 231 * 232 * @param string $content HTML DOM 233 * @param string $class Class to add DOM elements 234 * @return string HTML DOM with class added 235 */ 236 public function elementAddClass($html, $class) { 237 preg_match('/class.*?".*?"/', $html, $matches); 238 if(count($matches) > 0) { 239 preg_match('/[" ]'.$class.'[" ]/', $matches[0], $matches); 240 if(count($matches) == 0) { 241 return preg_replace('/(class.*?=.*?")/', '${1}'.$class.' ', $html, 1); 242 } 243 } else { 244 return preg_replace('/>/', 'class="'.$class.'">', $html, 1); 245 } 246 247 return $html; 248 } 249 250 251 /** 252 * Include Page 253 * 254 * @author James Collins <james.collins@outlook.com.au> 255 * 256 * @param string $location 257 * @param boolean $return 258 * @return string 259 */ 260 public function includePage($location, $return = false) 261 { 262 263 $content = ''; 264 265 if($content === '') $content = tpl_include_page($location, 0, 1, $this->getConf('useACL')); 266 267 if($content === '') return ''; 268 269 $content = $this->parseContent($content); 270 271 if($return) return $content; 272 273 print $content; 274 return ''; 275 } 276 277 public function includeLoggedIn() { 278 if (!empty($_SERVER['REMOTE_USER'])) { 279 echo '<li class="user navbar-text text-nowrap">'; 280 tpl_userinfo(); /* 'Logged in as ...' */ 281 echo '</li>'; 282 } 283 } 284 285 286 /** 287 * Include Menus 288 * 289 * @author James Collins <james.collins@outlook.com.au> 290 * 291 * @param string $location 292 */ 293 public function includeMenu($location) { 294 global $lang; 295 global $USERINFO; 296 297 $conf = $this->getConf('navbarIconsText'); 298 $dropdown = $this->getConf('navbarUseDropdown'); 299 $hideIcons = ($conf == 'text'); 300 $hideText = ($conf == 'icons'); 301 $guestMode = $this->getConf('navbarGuestHide') && ($USERINFO == false); 302 303 if(!$hideText && !$hideIcons) $hideText = false; 304 305 if(!$guestMode) { 306 // Page tools 307 $items = (new \dokuwiki\Menu\PageMenu())->getItems(); 308 print '<li id="dokuwiki__pagetools" class="nav-item dropdown"><a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">' . (!$hideIcons ? $this->icon('file') : '') . (!$hideText ? $lang['page_tools'] : '') . '</a><div class="dropdown-menu dropdown-menu-right">'; 309 foreach($items as $item) { 310 if($item->getType() != 'top') { 311 print '<a class="' . ($dropdown ? 'dropdown-item' : 'nav-item nav-link') . '" href="'.$item->getLink().'" title="'.$item->getTitle().'">'; 312 if(!$hideIcons) print '<span class="icon">'.inlineSVG($item->getSvg()).'</span>'; 313 if(!$hideText || $dropdown) print '<span>' . $item->getLabel() . '</span>'; 314 print '</a>'; 315 } 316 } 317 if($dropdown) print '</div></li>'; 318 319 // Site tools 320 $items = (new \dokuwiki\Menu\SiteMenu())->getItems('action '); 321 322 print '<li id="dokuwiki__sitetools" class="nav-item dropdown"><a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">' . (!$hideIcons ? $this->icon('gear') : '') . (!$hideText ? $lang['site_tools'] : '') . '</a><div class="dropdown-menu dropdown-menu-right">'; 323 foreach($items as $item) { 324 print '<a class="' . ($dropdown ? 'dropdown-item' : 'nav-item nav-link') . '" href="'.$item->getLink().'" title="'.$item->getTitle().'">'; 325 if(!$hideIcons) print '<span class="icon">'.inlineSVG($item->getSvg()).'</span>'; 326 if(!$hideText || $dropdown) print '<span>' . $item->getLabel() . '</span>'; 327 print '</a>'; 328 } 329 if($dropdown) print '</div></li>'; 330 } 331 332 // User tools 333 $items = (new \dokuwiki\Menu\UserMenu())->getItems('action'); 334 if(!$guestMode) print '<li id="dokuwiki__usertools" class="nav-item dropdown"><a class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">' . (!$hideIcons ? $this->icon('user') : '') . (!$hideText ? $lang['user_tools'] : '') . '</a><div class="dropdown-menu dropdown-menu-right">'; 335 foreach($items as $item) { 336 if(!$guestMode || $item->getType() == 'login') { 337 print '<a class="' . (($dropdown && !$guestMode) ? 'dropdown-item' : 'nav-item nav-link') . '" href="'.$item->getLink().'" title="'.$item->getTitle().'">'; 338 if(!$hideIcons) print '<span class="icon">'.inlineSVG($item->getSvg()).'</span>'; 339 if(!$hideText || ($dropdown && !$guestMode)) print '<span>' . $item->getLabel() . '</span>'; 340 print '</a>'; 341 } 342 } 343 if($dropdown && !$guestMode) print '</div></li>'; 344 345 } 346 347 /** 348 * Include Sidebar 349 * 350 * @author James Collins <james.collins@outlook.com.au> 351 * 352 * @param string $type Sidebar type 353 * @return boolean If sidebar was added 354 */ 355 public function includeSidebar($type) { 356 global $conf; 357 global $ID; 358 359 $useACL = true; // Add these as config options? 360 $propagate = true; 361 $checkPropagate = true; // Add these as config options? 362 363 switch($type) { 364 case 'left': 365 if($this->getConf('showSidebar') && page_findnearest($conf['sidebar'], $useACL) != false && p_get_metadata($ID, 'nosidebar', false) == false) { 366 $sidebar = tpl_includeFile('sidebarheader.html', false); 367 368 $sidebar .= $this->includeSearch('sidebar-top', false); 369 370 $confSidebar = tpl_include_page($conf['sidebar'], false, $propagate, $useACL); 371 if($checkPropagate && $confSidebar == '') { 372 $confSidebar = tpl_include_page($conf['sidebar'], false, false, $useACL); 373 } 374 $sidebar .= $confSidebar; 375 376 $sidebar .= $this->includeSearch('sidebar-bottom', false); 377 $sidebar .= tpl_includeFile('sidebarfooter.html', false); 378 379 if($sidebar != '') { 380 print '<aside class="col-md-2">' . $sidebar . '</aside>'; 381 } 382 383 return true; 384 } 385 386 return false; 387 } 388 389 return false; 390 } 391 392 /** 393 * Include Page Tools 394 * 395 * @author James Collins <james.collins@outlook.com.au> 396 * 397 * @param string $location Page tools location 398 * @return boolean If page tools was added 399 */ 400 public function includePageTools($location) { 401 $id = ''; 402 $group_class = 'btn-group'; 403 404 if((!$this->getConf('hidePageTools') && $location == 'side') || (!$this->getConf('hidePageToolsFooter') && $location == 'footer')) { 405 if($location == 'side') { 406 $id = 'dw__pagetools'; 407 $group_class = 'btn-group-vertical'; 408 } 409 410 print '<nav id="' . $id . '" class="hidden-print dw__pagetools">'; 411 print '<div class="' . $group_class . '">'; 412 413 $items = (new \dokuwiki\Menu\PageMenu())->getItems(); 414 foreach($items as $item) { 415 print '<a class="btn btn-sm btn-light" href="'.$item->getLink().'" title="'.$item->getTitle().'">' 416 .'<span class="icon">'.inlineSVG($item->getSvg()).'</span>' 417 . '<span class="a11y">'.$item->getLabel().'</span>' 418 . '</a>'; 419 } 420 421 print '</div>'; 422 print '</nav>'; 423 } 424 } 425 426 /** 427 * Include Search 428 * 429 * @author James Collins <james.collins@outlook.com.au> 430 * 431 * @param string $location Search location 432 * @return boolean If search was added 433 */ 434 public function includeSearch($location, $print=true) { 435 global $lang; 436 437 //<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search"> 438 $out = ''; 439 440 if($location == $this->getConf('navbarSearchPosition') || ($location == 'footer' && $this->getConf('showSearchInFooter')) || ($location == 'sidebar-top' && $this->getConf('showSearchInSidebar') == 'top') || ($location == 'sidebar-bottom' && $this->getConf('showSearchInSidebar') == 'bottom')) { 441 $out .= '<form action="' . wl($ID) . '" accept-charset="utf-8" class="form-inline search" id="dw__search" method="get" role="search">'; 442 $out .= '<div class="input-group"><input id="sqsearch" autocomplete="off" type="search" placeholder="' . $lang['btn_search'] . '" value="' . (($ACT == 'search') ? htmlspecialchars($QUERY) : '') . '" accesskey="f" name="q" class="form-control" title="[F]" style="height:auto"/>'; 443 $out .= '<div class="input-group-append"><button class="btn btn-secondary" type="submit" title="' . $lang['btn_search'] . '">'; 444 $out .= $this->icon('search'); //$lang['btn_search']; // TODO show icon if conf says and font awesome installed 445 $out .= '</button></div></div>'; 446 $out .= '<input type="hidden" name="do" value="search" />'; 447 $out .= '</form>'; 448 } 449 450 if($print) { 451 print $out; 452 return ''; 453 } 454 455 return $out; 456 } 457 458 459 /** 460 * Include Custom menus 461 * 462 * @author James Collins <james.collins@outlook.com.au> 463 * 464 * @param string $location menu location 465 * @return boolean If menu was added 466 */ 467 public function includeCustomMenu($location, $addOuter=true) { 468 if(($location == 'navbar' && $this->getConf('showCustomPagesInNavbar')) || ($location == 'footer' && $this->getConf('showCustomPagesInFooter'))) { 469 if($addOuter) { 470 print '<ul class="nav">'; 471 } 472 473 $menuList = $this->getConf('navbarCustomPages'); 474 475 if($menuList != '') { 476 $menuList = explode(',', $menuList); 477 478 foreach($menuList as $item) { 479 $i = strpos($item, '|'); 480 if($i !== false) { 481 $url = $this->getLink(trim(substr($item, 0, $i))); 482 $title = trim(substr($item, $i + 1)); 483 484 print('<li class="nav-item"><a href="' . $url . '" class="nav-link">' . $title . '</a></li>'); 485 } 486 } 487 } 488 489 if($addOuter) { 490 print '</ul>'; 491 } 492 } 493 } 494 495 /** 496 * Print out breadcrumbs 497 * 498 * @author James Collins <james.collins@outlook.com.au> 499 * 500 * @param string $location Location of breadcrumbs 501 */ 502 public function includeBreadcrumbs($location) { 503 if($location == $this->getConf('breadcrumbsLoc')) { 504 global $conf; 505 506 print '<div class="mikio-breadcrumbs">'; 507 508 if($conf['breadcrumbs']) { 509 tpl_breadcrumbs(); 510 } 511 512 if($conf['youarehere']) { 513 tpl_youarehere(); 514 } 515 516 print '</div>'; 517 } 518 } 519 520 521 /** 522 * Print out hero 523 * 524 * @author James Collins <james.collins@outlook.com.au> 525 */ 526 public function includeHero() { 527 global $ACT; 528 global $INFO; 529 530 // file_put_contents('output.txt', print_r($INFO, true)); 531 532 if($ACT == 'show') { 533 if($this->getConf('useHeroTitle')) { 534 print '<div class="mikio-hero d-flex flex-row">'; 535 print '<div class="mikio-hero-text flex-grow-1">'; 536 $this->includeBreadcrumbs('hero'); 537 print '<h1 id="mikio-hero-title">'; 538 print tpl_pagetitle(null, true).' '; // No idea why this requires a blank space afterwards to work? 539 print '</h1>'; 540 print '<h2 class="mikio-hero-subtext">'; 541 // print $this->heroSubtitle; // TODO scrape page for hero subtitle 542 print '</h2>'; 543 print '</div>'; 544 545 546 $hero_image = tpl_getMediaFile(array(':' . $INFO['namespace'] . ':hero.png', ':' . $INFO['namespace'] . ':hero.jpg', ':hero.png', ':hero.jpg', ':wiki:hero.png', ':wiki:hero.jpg', 'images/hero.png', 'images/hero.jpg'), false); 547 if($hero_image != '') $hero_image = ' style="background-image:url(\''.$hero_image.'\');"'; 548 549 print '<div class="mikio-hero-image"' . $hero_image . '></div>'; 550 print '</div>'; 551 } 552 } 553 } 554 555 556 /** 557 * Print out TOC 558 * 559 * @author James Collins <james.collins@outlook.com.au> 560 */ 561 public function includeTOC($location) { 562 if($this->getConf('tocfullheight') && $location === 'full') { 563 $toc = tpl_toc(true); 564 565 if($toc != '') { 566 print '<div class="mikio-toc mikio-toc-full">'; 567 print $toc; 568 print '</div>'; 569 } 570 } else if(!$this->getConf('tocfullheight') && $location === 'float') { 571 $toc = tpl_toc(true); 572 573 if($toc != '') { 574 print '<div class="mikio-toc mikio-toc-float">'; 575 print $toc; 576 print '</div>'; 577 } 578 } 579 } 580 581 582 /** 583 * Parse HTML for bootstrap 584 * 585 * @author James Collins <james.collins@outlook.com.au> 586 * 587 * @param string $content HTML content to parse 588 * @return string Parsed HTML for bootstrap 589 */ 590 public function parseContent($content) { 591 $html = new \simple_html_dom; 592 $html->load($content, true, false); 593 594 # Return original content if Simple HTML DOM fail or exceeded page size (default MAX_FILE_SIZE => 600KB) 595 if (!$html) { 596 return $content; 597 } 598 599 # Hide page title if hero is enabled 600 if($this->getConf('useHeroTitle')) { 601 $pageTitle = tpl_pagetitle(null, true); 602 603 foreach($html->find('h1,h2,h3,h4') as $elm) { 604 if($elm->innertext == $pageTitle) { 605 $elm->innertext = ''; 606 break; 607 } 608 } 609 } 610 611 # Hero subtitle 612 foreach($html->find('p') as $elm) { 613 $i = stripos($elm->innertext, '~~hero-subtitle'); 614 if($i !== false) { 615 $j = strpos($elm->innertext, '~~', $i + 2); 616 if($j !== false) { 617 if($j > $i + 16) { 618 $subtitle = substr($elm->innertext, $i + 16, $j - $i - 16); 619 foreach($html->find('.mikio-hero-subtext') as $subtitleElm) { 620 $subtitleElm->innertext = $subtitle; 621 } 622 } 623 624 $elm->innertext = substr($elm->innertext, $j + 2); 625 break; 626 } 627 } 628 } 629 630 # Buttons 631 foreach ($html->find('.button') as $elm) { 632 if ($elm->tag == 'form') { 633 continue; 634 } 635 $elm->class .= ' btn'; 636 } 637 638 foreach ($html->find('[type=button], [type=submit], [type=reset]') as $elm) { 639 if(stripos($elm->class, 'btn') === false) { 640 $elm->class .= ' btn btn-outline-secondary'; 641 } 642 } 643 644 # Section Edit Button 645 foreach ($html->find('.btn_secedit [type=submit]') as $elm) { 646 $elm->class .= ' btn-sm'; 647 } 648 649 # Section Edit icons 650 foreach ($html->find('.secedit.editbutton_section button') as $elm) { 651 $elm->innertext = '<i class="fa fa-edit" aria-hidden="true"></i> ' . $elm->innertext; 652 } 653 654 $content = $html->save(); 655 656 $html->clear(); 657 unset($html); 658 659 return $content; 660 } 661 662 663 /*** GET LINK ***/ 664 public function getLink($str) { 665 $i = strpos($str, '://'); 666 if($i !== false) return $str; 667 668 return wl($str); 669 } 670} 671 672global $TEMPLATE; 673 674$TEMPLATE = \dokuwiki\template\mikio\Template::getInstance();