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 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 moeglicherweise 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 case 'pagebreak' : $renderer->meta['sitetoc']['pagebreak'] = true; break; 142 } 143 } 144 } 145 146 $renderer->section_open("1 sitetoc"); 147 if ($renderer->meta['sitetoc']['noTocHeader'] === false) { 148 $renderer->header($lang['toc'], 1, $data['pos']); 149 } 150 151 return true; 152 } else 153 154 // All Output has been done 155 if (!is_array($data) && $data == 'save__meta') { 156 157 // Close TOC 158 $renderer->section_close(); 159 160 if ($renderer->meta['sitetoc']['noTOC'] === true) { 161 $renderer->doc = preg_replace("/<div.*?sitetoc.*?$/si", "", $renderer->doc); 162 } 163 164 // If this is not set, we may have it as Metadata 165 if (!$this->mergedPages && $renderer->meta['sitetoc']['mergeDoc']) { 166 $toc = $renderer->meta['sitetoc']['siteexportTOC']; 167 168 if (is_array($toc)) { 169 foreach ($toc as $tocItem) { 170 $this->mergedPages[] = array($tocItem['id'], $tocItem['depth']); 171 } 172 } 173 174 } 175 176 // If there is some data to be merged 177 if (count($this->mergedPages) > 0) { 178 179 $renderer->doc = ''; // Start fresh! 180 181 $renderer->section_open("1 mergedsite"); 182 183 // Prepare lookup Array 184 foreach ($this->mergedPages as $tocItem) { 185 $this->includedPages[] = array_shift(explode('#', $tocItem[0])); 186 } 187 188 // Load the instructions 189 $instr = array(); 190 foreach ($this->mergedPages as $tocElement) { 191 192 list($tocItem, $depth) = $tocElement; 193 $file = wikiFN($tocItem); 194 195 if (@file_exists($file)) { 196 $instructions = p_cached_instructions($file, false, $tocItem); 197 } else { 198 $instructions = p_get_instructions(io_readWikiPage($file, $tocItem)); 199 } 200 201 // Convert Link and header instructions 202 $instructions = $this->_convertInstructions($instructions, $addID, $renderer, $depth); 203 204 if ($renderer->meta['sitetoc']['mergeHeader'] && !empty($instr)) { 205 // Merge 206 $instr = $this->_mergeWithHeaders($instr, $instructions, 1); 207 } else 208 if ($renderer->meta['sitetoc']['pagebreak']) { 209 $instr = array_merge($instr, $instructions, $this->_convertInstructions(p_get_instructions('<sitepagebreak>'), $addID, $renderer, $depth)); 210 } else { 211 // Concat 212 $instr = array_merge($instr, $instructions); 213 } 214 } 215 216 if (!empty($instr)) { 217 $this->_cleanInstructions($instr, '/section_(close|open)/'); 218 $this->_cleanInstructions($instr, '/listu_(close|open)/'); 219 $this->_cleanInstructions($instr, '/listo_(close|open)/'); 220 221 //if its the document start, cut off the first element(document information) 222 if ($instr[count($instr)-1][1][0] == 'siteexport_pagebreak') { 223 $instr = array_slice($instr, 0, -1); 224 } 225 226 $this->_render_output($renderer, $mode, $instr); 227 } 228 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[] = array($SID, $DEPTH); 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 246 } else { 247 // // print normal internal link (XHTML odt) 248 $renderer->internallink($LNID, $NAME, null); 249 250 // Display Description underneath 251 if ($renderer->meta['sitetoc']['showDescription'] === true) { 252 // $renderer->p_open(); 253 $renderer->cdata(p_get_metadata($SID, 'description abstract', true)); 254 // $renderer->p_close(); 255 } 256 } 257 258 // Render Metadata 259 } else if ($mode == 'metadata') { 260 if (!is_array($data) && $data == 'save__meta') { 261 $renderer->meta['sitetoc']['siteexportTOC'] = $this->savedToc; 262 263 foreach ($this->savedToc as $page) { 264 $renderer->meta['relation']['references'][$page['id']] = $page['exists']; 265 } 266 267 $this->savedToc = array(); 268 } else if (!isset($data['start']) && !isset($data['pos'])) { 269 $this->savedToc[] = $this->__addTocItem($SID, $NAME, $DEPTH, $renderer); 270 } 271 } else { 272 return false; 273 } 274 275 return true; 276 } 277 278 /* 279 * pull apart the ID and create an Entry for the TOC 280 */ 281 function __addTocItem($id, $name, $depth, $renderer) { 282 global $conf; 283 global $ID; 284 285 // Render Title 286 $default = $renderer->_simpleTitle($id); 287 $exists = false; $isImage = false; $linktype = null; 288 resolve_pageid(getNS($ID), $id, $exists); 289 $name = $renderer->_getLinkTitle($name, $default, $isImage, $id, $linktype); 290 291 //keep hash anchor 292 list($id, $hash) = explode('#', $id, 2); 293 if (!empty($hash)) $hash = $renderer->_headerToLink($hash); 294 295 // Build Sitetoc Item 296 $item = array(); 297 $item['id'] = $id; 298 $item['name'] = $name; 299 $item['anchor'] = $hash; 300 $item['depth'] = $depth; 301 $item['exists'] = $exists; 302 if (!$conf['skipacl'] && auth_quickaclcheck($item['id']) < AUTH_READ) { 303 return false; 304 } 305 306 return $item; 307 } 308 309 /* 310 * Render the output of one page 311 */ 312 function _render_output($renderer, $mode, $instr) { 313 global $ID; 314 315 // Section IDs 316 // $addID = sectionID($addID, $check); //not possible to use a:b:c for id 317 318 if ($mode == 'xhtml') { 319 320 //--------RENDER 321 //renderer information(TOC build / Cache used) 322 $info = array(); 323 $content = p_render($mode, $instr, $info); 324 325 //Remove TOC`s, section edit buttons and tags 326 $content = $this->_cleanXHTML($content); 327 328 // embed the included page 329 // $renderer->doc .= '<div class="include">'; 330 //add an anchor to find start of a inserted page 331 // $renderer->doc .= "<a name='$addID' id='$addID'>"; 332 $renderer->doc .= $content; 333 // $renderer->doc .= '</div>'; 334 } else if ($mode == 'odt') { 335 336 // Loop through the instructions 337 foreach ($instr as $instruction) { 338 // Execute the callback against the Renderer 339 call_user_func_array(array($renderer, $instruction[0]), $instruction[1]); 340 } 341 } 342 } 343 344 /* 345 * Corrects relative internal links and media and 346 * converts headers of included pages to subheaders of the current page 347 */ 348 function _convertInstructions($instr, $id, &$renderer, $depth = 1) { 349 global $ID; 350 global $conf; 351 352 $n = count($instr); 353 354 for ($i = 0; $i < $n; $i++) { 355 //internal links(links inside this wiki) an relative links 356 if ((substr($instr[$i][0], 0, 12) == 'internallink')) { 357 $this->_convert_link($renderer, $instr[$i], $id); 358 } 359 else if ((substr($instr[$i][0], 0, 13) == 'internalmedia')) { 360 $this->_convert_media($renderer, $instr[$i], $id); 361 } 362 else if ((substr($instr[$i][0], 0, 6) == 'header')) { 363 $this->_convert_header($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1 364 } 365 else if ((substr($instr[$i][0], 0, 12) == 'section_open')) { 366 $this->_convert_section($renderer, $instr[$i], $depth-1); // -1 because the depth starts at 1 367 } 368 } 369 370 //if its the document start, cut off the first element(document information) 371 if ($instr[0][0] == 'document_start') 372 return array_slice($instr, 1, -1); 373 else 374 return $instr; 375 } 376 377 /* 378 * Convert link of given instruction 379 */ 380 function _convert_link(&$renderer, &$instr, $id) { 381 global $ID; 382 383 $exists = false; 384 385 resolve_pageid(getNS($id), $instr[1][0], $exists); 386 list($pageID, $pageReference) = explode("#", $instr[1][0], 2); 387 388 if (in_array($pageID, $this->includedPages)) { 389 // Crate new internal Links 390 $check = null; 391 392 // Either get existing reference or create from first heading. If still not there take the alternate ID 393 $pageNameLink = empty($pageReference) ? sectionID($pageID, $check) : $pageReference; 394 395 $instr[1][0] = $ID . "#" . $pageNameLink; 396 397 } else { 398 // Convert external Links to plain Text 399 400 $instr = array( 401 "cdata", 402 array($instr[1][1]), 403 $instr[2] 404 ); 405 } 406 } 407 408 /* 409 * Convert internalmedia of given instruction 410 */ 411 function _convert_media(&$renderer, &$instr, $id) { 412 global $ID; 413 414 // Resolvemedia returns the absolute path to media by reference 415 $exists = false; 416 resolve_mediaid(getNS($id), $instr[1][0], $exists); 417 } 418 419 /** 420 * @param integer $depth 421 */ 422 function _convert_header(&$renderer, &$instr, $depth) { 423 // More Depth! 424 $instr[1][1] += $depth; 425 } 426 427 /** 428 * @param integer $depth 429 */ 430 function _convert_section(&$renderer, &$instr, $depth) { 431 // More Depth! 432 $instr[1][0] += $depth; 433 } 434 435 function _mergeWithHeaders($existing, $newInstructions, $level = 1) { 436 437 $returnInstructions = array(); 438 $preparedInstructions = array(); 439 $existingStart = $existingEnd = 0; 440 $firstRun = true; 441 442 while ($this->_findNextHeaderSection($existing, $level, $existingStart, $existingEnd)) { 443 444 if ($firstRun) { 445 $returnInstructions = array_merge($returnInstructions, array_slice($existing, 0, $existingStart)); 446 $firstRun = false; 447 } 448 449 $currentSlice = array_slice($existing, $existingStart, $existingEnd-$existingStart); 450 451 // Find matching part with headername 452 $newStart = $newEnd = 0; 453 if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd, $currentSlice[0][1][0])) { 454 455 $newSlice = array_slice($newInstructions, $newStart, $newEnd-$newStart); 456 if ($newSlice[0][0] == 'header') 457 array_shift($newSlice); // Remove Heading 458 459 // merge found parts on next level. 460 $returnedInstructions = $this->_mergeWithHeaders($currentSlice, $newSlice, $level+1); 461 462 // Put them at the end! 463 $preparedInstructions = array_merge($preparedInstructions, $returnedInstructions); 464 465 // Remove from input 466 array_splice($newInstructions, $newStart, $newEnd-$newStart); 467 } else { 468 $preparedInstructions = array_merge($preparedInstructions, $currentSlice); 469 } 470 471 $existingStart = $existingEnd; 472 } 473 474 // Append the rest 475 $returnInstructions = array_merge($returnInstructions, array_slice($existing, $existingStart)); 476 477 // Check for section close inconsistencies and put one at the very end ... 478 $section_postpend = array(); 479 if ( 480 ( 481 ($tmp = array_slice($newInstructions, -1)) 482 && ($tmp[0][0] == 'section_close') 483 ) 484 && 485 ( 486 ($tmp = array_slice($newInstructions, -2)) 487 && ($tmp[0][0] == 'section_close') 488 ) 489 ) { 490 $section_postpend = array_splice($newInstructions, -1); 491 } 492 if ( 493 ( 494 ($tmp = array_slice($returnInstructions, -1)) 495 && ($tmp[0][0] == 'section_close') 496 ) 497 && 498 ( 499 ($tmp = array_slice($returnInstructions, -2)) 500 && ($tmp[0][0] == 'section_close') 501 ) 502 ) { 503 $section_postpend = array_merge($section_postpend, array_splice($returnInstructions, -1)); 504 } 505 506 // What if there are headings left inside the $newInstructions????? 507 // Find matching part with headername 508 $newStart = $newEnd = 0; 509 $section_prepend = array(); 510 if ($this->_findNextHeaderSection($newInstructions, $level, $newStart, $newEnd)) { 511 // If there are header in here, build a prepend and have the rest at the end 512 $section_prepend = array_splice($newInstructions, 0, $newStart); 513 } else { 514 // If not, prepend all of it. 515 $section_prepend = $newInstructions; 516 $newInstructions = array(); 517 } 518 519 $returnInstructions = array_merge($returnInstructions, $section_prepend, $preparedInstructions, $newInstructions, $section_postpend); 520 521 return $returnInstructions; 522 } 523 524 /** 525 * @param integer $level 526 */ 527 function _findNextHeaderSection($section, $level, &$start, &$end, $headerName = null) { 528 529 $inCount = count($section); 530 $currentSlice = -1; 531 532 // Find Level 1 Header that matches. 533 for ($i = $start; $i < $inCount; $i++) { 534 535 $instruction = $section[$i]; 536 $end = $i; // Or it will be lost and a section close will be missing. 537 538 // First Level Header 539 if ($instruction[0] == 'header' && $instruction[1][1] == $level) { 540 541 if ($currentSlice > 0) { 542 return true; 543 } 544 545 if ($headerName == null || ($headerName == $instruction[1][0])) { 546 // Begin of new slice ... 547 $start = $currentSlice = $i; 548 } 549 } 550 } 551 552 // Nothing found 553 $end = $i; // Or it will be lost and a section close will be missing. 554 return $currentSlice > 0; 555 } 556 557 /** 558 * @param string $tag 559 */ 560 function _cleanInstructions(&$instructions, $tag) { 561 562 $inCount = count($instructions); 563 for ($i = 0; $i < $inCount; $i++) { 564 565 // Last instruction 566 if ($i == $inCount-1) { 567 break; 568 } 569 570 if (preg_match($tag, $instructions[$i][0]) && preg_match($tag, $instructions[$i+1][0]) && $instructions[$i][0] != $instructions[$i+1][0]) { 571 572 // found different tags, but both match the expression and follow each other - so they can be elliminated 573 array_splice($instructions, $i, 2); 574 $inCount -= 2; 575 $i--; 576 } 577 578 } 579 } 580 581 /** 582 * Remove TOC, section edit buttons and tags 583 */ 584 function _cleanXHTML($xhtml) { 585 $replace = array( 586 '!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove TOCs 587 '#<!-- SECTION \[(\d*-\d*)\] -->#s' => '', // remove section edit buttons 588 '!<div id="tags">.*?(</div>)!s' => '' // remove category tags 589 ); 590 $xhtml = preg_replace(array_keys($replace), array_values($replace), $xhtml); 591 return $xhtml; 592 } 593 594 /** 595 * Allow the plugin to prevent DokuWiki creating a second instance of itself 596 * 597 * @return bool true if the plugin can not be instantiated more than once 598 */ 599 function isSingleton() { 600 return true; 601 } 602} 603// vim:ts=4:sw=4:et:enc=utf-8: 604