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