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