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