1<?php 2/** 3 * Search with Scopes 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author i-net software <tools@inetsoftware.de> 7 * @author Gerry Weissbach <gweissbach@inetsoftware.de> 8 */ 9 10// must be run within Dokuwiki 11if (!defined('DOKU_INC')) die(); 12if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 13 14require_once(DOKU_PLUGIN . 'syntax.php'); 15 16class syntax_plugin_siteexport_toc extends DokuWiki_Syntax_Plugin { 17 18 private $insideToc = false; 19 private $savedToc = array(); 20 private $options = array(); 21 22 private $mergedPages = array(); 23 private $includedPages = array(); 24 private $merghintIds = array(); 25 26 function getType() { return 'protected'; } 27 function getPType() { return 'block'; } 28 function getAllowedTypes() { return array('container'); } 29 function getSort() { return 100; } 30 31 /** 32 * Connect pattern to lexer 33 */ 34 function connectTo($mode) { 35 $this->Lexer->addEntryPattern('<toc>(?=.*?</toc>)', $mode, 'plugin_siteexport_toc'); 36 $this->Lexer->addEntryPattern('<toc .+?>(?=.*?</toc>)', $mode, 'plugin_siteexport_toc'); 37 $this->Lexer->addSpecialPattern("\[\[.+?\]\]", $mode, 'plugin_siteexport_toc'); 38 } 39 40 function postConnect() { 41 $this->Lexer->addExitPattern('</toc.*?>', 'plugin_siteexport_toc'); 42 } 43 44 function handle($match, $state, $pos, Doku_Handler $handler) { 45 global $ID, $INFO; 46 47 switch ($state) { 48 case DOKU_LEXER_ENTER: 49 50 $this->insideToc = true; 51 $this->options = explode(' ', substr($match, 5, -1)); 52 return array('start' => true, 'pos' => $pos, 'options' => $this->options); 53 54 case DOKU_LEXER_SPECIAL: 55 56 if ($this->insideToc) { 57 58 $link = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match); 59 // Split title from URL 60 $link = explode('|', $link, 2); 61 if (!isset($link[1])) { 62 $link[1] = NULL; 63 } else if (preg_match('/^\{\{[^\}]+\}\}$/', $link[1])) { 64 // If the title is an image, convert it to an array containing the image details 65 $link[1] = Doku_Handler_Parse_Media($link[1]); 66 } 67 $link[0] = trim($link[0]); 68 69 if (!(preg_match('/^[a-zA-Z0-9\.]+>{1}.*$/u', $link[0]) || 70 preg_match('/^\\\\\\\\[\w.:?\-;,]+?\\\\/u', $link[0]) || 71 preg_match('#^([a-z0-9\-\.+]+?)://#i', $link[0]) || 72 preg_match('<' . PREG_PATTERN_VALID_EMAIL . '>', $link[0]) || 73 preg_match('!^#.+!', $link[0])) 74 ) { 75 76 // Get current depth from call stack 77 $depth = 1; 78 if ($handler->CallWriter instanceof Doku_Handler_List) { 79 80 $calls = array_reverse($handler->CallWriter->calls); 81 $call = $calls[0]; 82 foreach ($calls as $item) { 83 if (in_array($item[0], array('list_item', 'list_open'))) { $call = $item; break; } 84 } 85 86 $depth = $handler->CallWriter->interpretSyntax($call[1][0], $listType = null)-1; // Minus one because of plus one inside the interpret function 87 } 88 89 if (empty($link[0])) { break; } // No empty elements. This would lead to problems 90 return array($link[0], $link[1], $depth); 91 } else { 92 // use parser! - but with another p 93 $handler->internallink($match, $state, $pos); 94 } 95 } else { 96 // use parser! 97 $handler->internallink($match, $state, $pos); 98 } 99 100 return false; 101 case DOKU_LEXER_UNMATCHED: 102 103 $handler->_addCall('cdata', array($match), $pos); 104 105 return false; 106 case DOKU_LEXER_EXIT: 107 108 $this->insideToc = false; 109 return 'save__meta'; 110 } 111 return false; 112 } 113 114 function render($mode, Doku_Renderer $renderer, $data) { 115 global $ID, $lang, $INFO; 116 117 list($SID, $NAME, $DEPTH) = $data; 118 119 resolve_pageid(getNS($ID), $SID, $exists = null); 120// $SID = cleanID($SID); // hier kein cleanID, da sonst moeglicherweise der anker verloren geht 121 122 // Render XHTML and ODT 123 if ($mode == 'xhtml' || $mode == 'odt') { 124 125 // TOC Title 126 if (is_array($data) && $data['start'] == true) { 127 128 if (is_Array($data['options'])) { 129 foreach ($data['options'] as $opt) { 130 switch ($opt) { 131 case 'description' : $renderer->meta['sitetoc']['showDescription'] = true; break; 132 case 'notoc' : $renderer->meta['sitetoc']['noTOC'] = true; break; 133 case 'merge' : $renderer->meta['sitetoc']['mergeDoc'] = true; break; 134 case 'nohead' : $renderer->meta['sitetoc']['noTocHeader'] = true; break; 135 case 'mergeheader' : $renderer->meta['sitetoc']['mergeHeader'] = true; break; 136 case 'pagebreak' : $renderer->meta['sitetoc']['pagebreak'] = true; break; 137 case 'mergehint' : $renderer->meta['sitetoc']['mergehint'] = true; break; 138 } 139 } 140 } 141 142 $renderer->section_open("1 sitetoc"); 143 if ($renderer->meta['sitetoc']['noTocHeader'] === false) { 144 $renderer->header($lang['toc'], 1, $data['pos']); 145 } 146 147 return true; 148 } else 149 150 // All Output has been done 151 if (!is_array($data) && $data == 'save__meta') { 152 153 // Close TOC 154 $renderer->section_close(); 155 156 if ($renderer->meta['sitetoc']['noTOC'] === true) { 157 $renderer->doc = preg_replace("/<div.*?sitetoc.*?$/si", "", $renderer->doc); 158 } 159 160 // If this is not set, we may have it as Metadata 161 if ( empty( $this->mergedPages ) && $renderer->meta['sitetoc']['mergeDoc']) { 162 $toc = $renderer->meta['sitetoc']['siteexportTOC']; 163 164 if (is_array($toc)) { 165 foreach ($toc as $tocItem) { 166 $this->mergedPages[] = array($tocItem['id'], $tocItem['depth']); 167 } 168 } 169 170 } 171 172 // If there is some data to be merged 173 if (count($this->mergedPages) > 0) { 174 175 $renderer->doc = ''; // Start fresh! 176 177 $renderer->section_open("1 mergedsite" . ($renderer->meta['sitetoc']['mergehint']?' mergehint':'')); 178 179 // Prepare lookup Array 180 foreach ($this->mergedPages as $tocItem) { 181 list($this->includedPages[]) = explode('#', $tocItem[0]); 182 } 183 184 // Load the instructions 185 $instr = array(); 186 foreach ($this->mergedPages as $tocElement) { 187 188 list($tocItem, $depth) = $tocElement; 189 $file = wikiFN($tocItem); 190 191 if (@file_exists($file)) { 192 $instructions = p_cached_instructions($file, false, $tocItem); 193 } else { 194 $instructions = p_get_instructions(io_readWikiPage($file, $tocItem)); 195 } 196 197 // Convert Link and header instructions 198 $instructions = $this->_convertInstructions($instructions, $addID = null, $renderer, $depth); 199 200 if ($renderer->meta['sitetoc']['mergeHeader'] && count($this->mergedPages) > 1 ) { 201 // get a hint for merged pages 202 if ($renderer->meta['sitetoc']['mergehint']) { 203 // only if the first section is already there 204 $mergeHint = p_get_metadata( $tocItem, 'mergehint', METADATA_RENDER_USING_SIMPLE_CACHE ); 205 if ( empty( $mergeHint) ) { $mergeHint = p_get_metadata( $tocItem, 'thema', METADATA_RENDER_USING_SIMPLE_CACHE ); } 206 if ( empty( $mergeHint) ) { $mergeHint = tpl_pagetitle( $tocItem, true ); } 207 $instructions = $this->_mergeWithHeaders( $this->_initialHeaderStructure( $instructions ), $instructions, 1, $mergeHint); 208 } 209 // Merge 210 $instr = $this->_mergeWithHeaders( $instr, $instructions, 1); 211 } else 212 if ($renderer->meta['sitetoc']['pagebreak']) { 213 $sitepagebreak = array( array( 214 'plugin', 215 array( 216 'siteexport_toctools', 217 array( 218 'pagebreak', 219 null, 220 null 221 ) 222 ) 223 )); 224 $instr = array_merge($instr, $instructions, $sitepagebreak); 225 } else { 226 // Concat 227 $instr = array_merge($instr, $instructions); 228 } 229 } 230 231 if (!empty($instr)) { 232 $this->_cleanAllInstructions($instr); 233 234 //if its the document start, cut off the first element(document information) 235 if ($instr[count($instr)-1][1][0] == 'siteexport_toctools') { 236 $instr = array_slice($instr, 0, -1); 237 } 238 239 240 // print "<pre>"; print_r($instr); print "</pre>"; 241 $this->_render_output($renderer, $mode, $instr); 242 } 243 244 $renderer->section_close(); 245 } 246 return true; 247 } 248 249 // Save the current ID 250 $LNID = $SID; 251 252 // Add ID to flags['mergeDoc'] 253 if ($renderer->meta['sitetoc']['mergeDoc'] === true) { // || (count($renderer->meta['sitetoc']['siteexportTOC']) > 0 && $renderer->meta['sitetoc']['siteexportMergeDoc'] === true) ) { 254 $this->mergedPages[] = array($SID, $DEPTH); 255 // $default = $renderer->_simpleTitle($SID); $isImage = false; 256 resolve_pageid(getNS($ID), $SID, $exists); 257 258 // $NAME = empty($NAME) ? p_get_first_heading($SID, true) : $NAME; 259 // $LNID = "$ID#" . sectionID($SID, $check); 260 261 } else { 262 // // print normal internal link (XHTML odt) 263 $renderer->internallink($LNID, $NAME, null); 264 265 // Display Description underneath 266 if ($renderer->meta['sitetoc']['showDescription'] === true) { 267 // $renderer->p_open(); 268 $renderer->cdata(p_get_metadata($SID, 'description abstract', true)); 269 // $renderer->p_close(); 270 } 271 } 272 273 // Render Metadata 274 } else if ($mode == 'metadata') { 275 if (!is_array($data) && $data == 'save__meta') { 276 $renderer->meta['sitetoc']['siteexportTOC'] = $this->savedToc; 277 278 foreach ($this->savedToc as $page) { 279 $renderer->meta['relation']['references'][$page['id']] = $page['exists']; 280 } 281 282 $this->savedToc = array(); 283 } else if (!isset($data['start']) && !isset($data['pos'])) { 284 $this->savedToc[] = $this->__addTocItem($SID, $NAME, $DEPTH, $renderer); 285 } 286 } else { 287 return false; 288 } 289 290 return true; 291 } 292 293 /* 294 * pull apart the ID and create an Entry for the TOC 295 */ 296 private function __addTocItem($id, $name, $depth, $renderer) { 297 global $conf; 298 global $ID; 299 300 // Render Title 301 $default = $renderer->_simpleTitle($id); 302 $exists = false; $isImage = false; $linktype = null; 303 resolve_pageid(getNS($ID), $id, $exists); 304 $name = $renderer->_getLinkTitle($name, $default, $isImage, $id, $linktype); 305 306 //keep hash anchor 307 list($id, $hash) = explode('#', $id, 2); 308 if (!empty($hash)) $hash = $renderer->_headerToLink($hash); 309 310 // Build Sitetoc Item 311 $item = array(); 312 $item['id'] = $id; 313 $item['name'] = $name; 314 $item['anchor'] = $hash; 315 $item['depth'] = $depth; 316 $item['exists'] = $exists; 317 if (!$conf['skipacl'] && auth_quickaclcheck($item['id']) < AUTH_READ) { 318 return false; 319 } 320 321 return $item; 322 } 323 324 /* 325 * Render the output of one page 326 */ 327 private function _render_output($renderer, $mode, $instr) { 328 global $ID; 329 330 // Section IDs 331 // $addID = sectionID($addID, $check); //not possible to use a:b:c for id 332 333 if ($mode == 'xhtml') { 334 335 //--------RENDER 336 //renderer information(TOC build / Cache used) 337 $info = array(); 338 $content = p_render($mode, $instr, $info); 339 340 //Remove TOC`s, section edit buttons and tags 341 $content = $this->_cleanXHTML($content); 342 343 // embed the included page 344 // $renderer->doc .= '<div class="include">'; 345 //add an anchor to find start of a inserted page 346 // $renderer->doc .= "<a name='$addID' id='$addID'>"; 347 $renderer->doc .= $content; 348 // $renderer->doc .= '</div>'; 349 } else if ($mode == 'odt') { 350 351 // Loop through the instructions 352 foreach ($instr as $instruction) { 353 // Execute the callback against the Renderer 354 call_user_func_array(array($renderer, $instruction[0]), $instruction[1]); 355 } 356 } 357 } 358 359 /* 360 * Corrects relative internal links and media and 361 * converts headers of included pages to subheaders of the current page 362 */ 363 private function _convertInstructions($instr, $id, &$renderer, $depth = 1) { 364 global $ID; 365 global $conf; 366 367 $n = count($instr); 368 369 for ($i = 0; $i < $n; $i++) { 370 //internal links(links inside this wiki) an relative links 371 if ((substr($instr[$i][0], 0, 12) == 'internallink')) { 372 $this->_convert_link($renderer, $instr[$i], $id); 373 } 374 else if ((substr($instr[$i][0], 0, 13) == 'internalmedia')) { 375 $this->_convert_media($renderer, $instr[$i], $id); 376 } 377 else if ((substr($instr[$i][0], 0, 6) == 'header')) { 378 $this->_convert_header($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1 379 } 380 else if ((substr($instr[$i][0], 0, 12) == 'section_open')) { 381 $this->_convert_section($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1 382 } 383 } 384 385 //if its the document start, cut off the first element(document information) 386 if ($instr[0][0] == 'document_start') 387 return array_slice($instr, 1, -1); 388 else 389 return $instr; 390 } 391 392 /* 393 * Convert link of given instruction 394 */ 395 private function _convert_link(&$renderer, &$instr, $id) { 396 global $ID; 397 398 $exists = false; 399 400 resolve_pageid(getNS($id), $instr[1][0], $exists); 401 list($pageID, $pageReference) = explode("#", $instr[1][0], 2); 402 403 if (in_array($pageID, $this->includedPages)) { 404 // Crate new internal Links 405 $check = null; 406 407 // Either get existing reference or create from first heading. If still not there take the alternate ID 408 $pageNameLink = empty($pageReference) ? sectionID($pageID, $check) : $pageReference; 409 410 $instr[1][0] = $ID . "#" . $pageNameLink; 411 412 } else { 413 // Convert external Links to plain Text 414 415 $instr = array( 416 "cdata", 417 array($instr[1][1]), 418 $instr[2] 419 ); 420 } 421 } 422 423 /* 424 * Convert internalmedia of given instruction 425 */ 426 private function _convert_media(&$renderer, &$instr, $id) { 427 global $ID; 428 429 // Resolvemedia returns the absolute path to media by reference 430 $exists = false; 431 resolve_mediaid(getNS($id), $instr[1][0], $exists); 432 } 433 434 /** 435 * @param integer $depth 436 */ 437 private function _convert_header(&$renderer, &$instr, $depth) { 438 // More Depth! 439 $instr[1][1] += $depth; 440 } 441 442 /** 443 * @param integer $depth 444 */ 445 private function _convert_section(&$renderer, &$instr, $depth) { 446 // More Depth! 447 $instr[1][0] += $depth; 448 } 449 450 private function _mergeWithHeaders($existing, $newInstructions, $level = 1, $mergeHint = array() ) { 451 452 $returnInstructions = array(); 453 $preparedInstructions = array(); 454 $existingStart = $existingEnd = 0; 455 $firstRun = true; 456 457 while ($this->_findNextHeaderSection($existing, $level, $existingStart, $existingEnd)) { 458 459 if ($firstRun) { 460 $returnInstructions = array_merge($returnInstructions, array_slice($existing, 0, $existingStart)); 461 $firstRun = false; 462 } 463 464 $currentSlice = array_slice($existing, $existingStart, $existingEnd-$existingStart); 465 466 // Find matching part with headername 467 $newStart = $newEnd = 0; 468 if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd, $currentSlice[0][1][0])) { 469 470 $newSlice = array_slice($newInstructions, $newStart, $newEnd-$newStart); 471 if ($newSlice[0][0] == 'header') 472 array_shift($newSlice); // Remove Heading 473 474 // merge found parts on next level. 475 $returnedInstructions = $this->_mergeWithHeaders($currentSlice, $newSlice, $level+1, $mergeHint); 476 477 // Put them at the end! 478 $preparedInstructions = array_merge($preparedInstructions, $returnedInstructions); 479 480 // Remove from input 481 array_splice($newInstructions, $newStart, $newEnd-$newStart); 482 } else { 483 // Nothing else found 484 $preparedInstructions = array_merge($preparedInstructions, $currentSlice); 485 } 486 487 $existingStart = $existingEnd; 488 } 489 490 // Append the rest 491 $returnInstructions = array_merge($returnInstructions, array_slice($existing, $existingStart)); 492 493 // Check for section close inconsistencies and put one at the very end ... 494 $section_postpend = array(); 495 if ( 496 ( 497 ($tmp1 = array_slice($newInstructions, -1)) 498 && ($tmp1[0][0] == 'section_close') 499 ) 500 && 501 ( 502 ($tmp2 = array_slice($newInstructions, -2)) 503 && ($tmp2[0][0] == 'section_close') 504 ) 505 ) { 506 $section_postpend = array_splice($newInstructions, -1); 507 } 508 if ( 509 ( 510 ($tmp3 = array_slice($returnInstructions, -1)) 511 && ($tmp3[0][0] == 'section_close') 512 ) 513 && 514 ( 515 ($tmp4 = array_slice($returnInstructions, -2)) 516 && ($tmp4[0][0] == 'section_close') 517 ) 518 ) { 519 $section_postpend = array_merge($section_postpend, array_splice($returnInstructions, -1)); 520 } 521 522 // What if there are headings left inside the $newInstructions????? 523 // Find matching part with headername 524 $newStart = $newEnd = 0; 525 $section_prepend = array(); 526 if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd)) { 527 // If there are header in here, build a prepend and have the rest at the end 528 $section_prepend = array_splice($newInstructions, 0, $newStart); 529 } else { 530 // If not, prepend all of it. 531 $section_prepend = $newInstructions; 532 $newInstructions = array(); 533 } 534 535 $this->_insertMergeHint( $section_prepend, $mergeHint ); 536 537 $returnInstructions = array_merge($returnInstructions, $section_prepend, $preparedInstructions, $newInstructions, $section_postpend); 538 539 return $returnInstructions; 540 } 541 542 /** 543 * @param integer $level 544 */ 545 private function _findNextHeaderSection($section, $level, &$start, &$end, $headerName = null) { 546 547 $inCount = count($section); 548 $currentSlice = -1; 549 550 // Find Level 1 Header that matches. 551 for ($i = $start; $i < $inCount; $i++) { 552 553 $instruction = $section[$i]; 554 $end = $i; // Or it will be lost and a section close will be missing. 555 556 // First Level Header 557 if ($instruction[0] == 'header' && $instruction[1][1] == $level) { 558 559 if ($currentSlice > 0) { 560 return true; 561 } 562 563 if ($headerName == null || ($headerName == $instruction[1][0])) { 564 // Begin of new slice ... 565 $start = $currentSlice = $i; 566 } 567 } 568 } 569 570 // Nothing found 571 $end = $i; // Or it will be lost and a section close will be missing. 572 return $currentSlice > 0; 573 } 574 575 private function _cleanAllInstructions(&$instr) { 576 $this->_cleanInstructions($instr, '/section_(close|open)/'); 577 $this->_cleanInstructions($instr, '/listu_(close|open)/'); 578 $this->_cleanInstructions($instr, '/listo_(close|open)/'); 579 } 580 581 /** 582 * @param string $tag 583 */ 584 private function _cleanInstructions(&$instructions, $tag) { 585 586 $inCount = count($instructions); 587 for ($i = 0; $i < $inCount; $i++) { 588 589 // Last instruction 590 if ($i == $inCount-1) { 591 break; 592 } 593 594 if (preg_match($tag, $instructions[$i][0]) && preg_match($tag, $instructions[$i+1][0]) && $instructions[$i][0] != $instructions[$i+1][0]) { 595 596 // found different tags, but both match the expression and follow each other - so they can be elliminated 597 array_splice($instructions, $i, 2); 598 $inCount -= 2; 599 $i--; 600 } 601 602 } 603 } 604 605 /** 606 * Strip everything except for the headers 607 */ 608 private function _initialHeaderStructure($instructions) { 609 $inCount = count($instructions); 610 for ($i = 0; $i < $inCount; $i++) { 611 612 // Last instruction 613 if ($i == $inCount-1) { 614 break; 615 } 616 617 if (!in_array($instructions[$i][0], array('header', 'section_open', 'section_close', 'p_open', 'p_close'))) { 618 // found non-matching 619 array_splice($instructions, $i, 1); 620 $inCount --; 621 $i--; 622 } 623 } 624 return $instructions; 625 } 626 627 private function _insertMergeHint( &$instructions, $mergeHint ) { 628 629 // Surround new slice with a mergehint 630 if ( empty( $mergeHint ) ) { return; } 631 632 // No emtpy insruction sets. 633 $this->_cleanAllInstructions( $instructions ); 634 if ( empty( $instructions ) ) { return; } 635 636 // only section content should be surrounded. 637 if ( $instructions[0][0] != 'section_open' ) { return; } 638 639 // save for later use 640 $mergeHints = array(); 641 $mergeHintId = sectionid( $mergeHint, $mergeHints ); 642 $this->merghintIds[$mergeHintId] = $mergeHint; 643 644 $mergeHintPrepend = array( array( 645 'plugin', 646 array( 647 'siteexport_toctools', 648 array( 649 'mergehint', 650 'start', 651 $mergeHint, 652 $mergeHintId 653 ) 654 ) 655 )); 656 657 $mergeHintPostpend = array( array( 658 'plugin', 659 array( 660 'siteexport_toctools', 661 array( 662 'mergehint', 663 'end', 664 $mergeHint 665 ) 666 ) 667 )); 668 669/* 670 print "\n\n#########\n"; 671 print_r($instructions); 672 print "\nn#########\n\n"; 673*/ 674 $instructions = array_merge( $mergeHintPrepend, $instructions, $mergeHintPostpend ); 675 } 676 677 /** 678 * Remove TOC, section edit buttons and tags 679 */ 680 function _cleanXHTML($xhtml) { 681 $replace = array( 682 '!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove TOCs 683 '#<!-- SECTION \[(\d*-\d*)\] -->#s' => '', // remove section edit buttons 684 '!<div id="tags">.*?(</div>)!s' => '' // remove category tags 685 ); 686 $xhtml = preg_replace(array_keys($replace), array_values($replace), $xhtml); 687 return $xhtml; 688 } 689 690 /** 691 * Allow the plugin to prevent DokuWiki creating a second instance of itself 692 * 693 * @return bool true if the plugin can not be instantiated more than once 694 */ 695 function isSingleton() { 696 return true; 697 } 698} 699// vim:ts=4:sw=4:et:enc=utf-8: 700