1<?php 2 3if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); 4if(!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN',DOKU_INC.'lib/plugins/'); 5require_once(DOKU_PLUGIN.'syntax.php'); 6require_once(DOKU_INC.'inc/search.php'); 7 8/* 9* {{compose> namespace#depth | print numbers how_deep absolute }} 10* 11* print: 12* print optimization 13* numbers + how_deep: 14* numbering for headers(1.1.3 hallo...) 15* absolute: 16* absolute indentation of headers, corresponding to file-depth in namespace 17* or (default) relative indentation 18*/ 19class syntax_plugin_composer extends DokuWiki_Syntax_Plugin { 20 21 /** 22 * how to handle <p> 23 */ 24 function getPType(){ 25 return 'block'; 26 } 27 28 /** 29 * what other types are allowed iside 30 */ 31 function getAllowedTypes() { 32 return array('substition'); 33 } 34 35 /** 36 * What kind of syntax are we? 37 */ 38 function getType(){ 39 return 'substition'; 40 } 41 42 /** 43 * Where to sort in? 44 */ 45 function getSort(){ 46 return 311; 47 } 48 49 /** 50 * Connect pattern to lexer 51 */ 52 function connectTo($mode) { 53 $this->Lexer->addSpecialPattern("{{compose>.*?}}",$mode,'plugin_composer'); 54 } 55 56 /** 57 * Handle the match 58 */ 59 function handle($match, $state, $pos, &$handler){ 60 61 global $conf; 62 63 $opt = array( 64 "level" => 0, 65 "nons" => false, 66 "relative" => false, 67 "print" => false, 68 "numbers" => false, 69 "numbers_depth" => 2, 70 "resort" => false 71 ); 72 73 $givenOptString = ""; 74 $givenOpt = array(); 75 $content = ""; 76 77 $match = preg_replace("/\{\{compose\>(.*)\}\}/", "$1", $match); 78 79 $nsOpt = preg_split("/\|/", $match); 80 81 if (count($nsOpt) == 1) { 82 83 $ns = $nsOpt[0]; 84 85 } else { 86 87 $ns = $nsOpt[0]; 88 $givenOptString = $nsOpt[1]; 89 $givenOpt = preg_split("/ /", $givenOptString); 90 91 } 92 93 $nsLevel = preg_split("/#/", $ns); 94 95 if (count($nsLevel) > 1) { 96 97 $ns = $nsLevel[0]; 98 $opt["level"] = $nsLevel[1]; 99 100 } 101 102 if (in_array("absolute", $givenOpt)) { 103 104 if ($conf["useslashes"]) { 105 106 $opt["relative"] = substr_count($ns, "/") + 1; 107 108 } else { 109 110 $opt["relative"] = substr_count($ns, ":") + 1; 111 112 } 113 114 } 115 116 if (in_array("nons", $givenOpt)) { 117 118 $opt["nons"] = true; 119 120 } 121 122 if (in_array("print", $givenOpt)) { 123 124 $opt["split_too_long"] = true; 125 126 } 127 128 // Do we have the numbers option without a level specification? 129 130 if (in_array("numbers", $givenOpt)) { 131 132 $opt["numbers"] = true; 133 134 } 135 136 // Do we have the numbers option with the level specification? 137 138 if (preg_match("/numbers:(\d)*/", $givenOptString, $matches)) { 139 140 $opt["numbers"] = true; 141 $opt["numbers_depth"] = $matches[1]; 142 143 } 144 145 if (in_array("resort", $givenOpt)) { 146 147 $opt["resort"] = true; 148 149 } 150 151 return array( 152 'namespace'=> $ns, 153 'options' => $opt 154 ); 155 } 156 157 /** 158 * Create output 159 */ 160 function render($mode, &$renderer, $data) { 161 if($mode == 'xhtml'){ 162 //for header numbering 163 $header_count = array( 1=>0, 164 2=>0, 165 3=>0, 166 4=>0, 167 5=>0, 168 ); 169 170 $data['header_count'] = &$header_count; 171 172 // get all files in the supplied namespace, with their level 173 $file_data = $this->_get_file_data($data); 174 175 /* 176 * Resort results to optimize output 177 * Sort by levels, then first sort the directories, 178 * than the start page and then the files 179 */ 180 181 if ($data["options"]["resort"]) { 182 183 usort($file_data, "composer_sort_filedata"); 184 185 } 186 187 foreach($file_data as $file) { 188 // build the output 189 $this->_render_output($renderer, $file, $data); 190 } 191 192 return true; 193 } 194 195 return false; 196 } 197 198 /* 199 * Get all files within the namespace 200 */ 201 function _get_file_data($data) { 202 global $conf; 203 204 $options = $data['options']; 205 $ns = $data['namespace']; 206 207 $resolvedPage = ""; 208 $pageExists = false; 209 210 // Reformat namespace to filename 211 212 // namespace options . or .. 213 214 $pageFile = wikiFN($ns); 215 216 if (!file_exists($pageFile)) { 217 218 // Try adding the start page 219 220 if ($conf["useslash"]) { 221 222 $ns .= "/"; 223 224 } else { 225 226 $ns .= ":"; 227 228 } 229 230 $ns .= $conf['start']; 231 232 $pageFile = wikiFN($ns); 233 234 if (!file_exists($pageFile)) { 235 236 return false; 237 238 } 239 240 } 241 242 $dirname = dirname($pageFile); 243 $dirname = str_replace($conf['datadir'], "", $dirname); 244 245 $file_data = array( 246 'start_lvl' => substr_count($dirname, DIRECTORY_SEPARATOR), 247 'blocked' => array(), //blocked files 248 ); 249 250 // find all matching files 251 252 search( 253 $file_data, 254 $conf['datadir'], 255 'composer_search_index', 256 $options, 257 $dirname 258 ); 259 260 // clean the file_data from non-files (for safety) 261 unset($file_data['start_lvl']); 262 unset($file_data['blocked']); 263 264 return $file_data; 265 } 266 267 268 /* 269 * Render the output 270 */ 271 272 function _render_output(&$renderer, $file_data, $o){ 273 274 // file attributes 275 $id = $file_data['id']; 276 $open = $file_data['open']; 277 $content = ""; 278 279 // build with relative indentation 280 281 $clevel = $file_data['level']; 282 283 if($o['options']['relative']){ 284 285 // Relative identation 286 287 $clevel -= $o['options']['relative']; 288 289 } 290 291 // Don't open the file (in case of directories and nons on) 292 293 if(!$open) { 294 295 return false; 296 297 } 298 299 // Convert a:b -> home/data/pages/a/b 300 301 $file = wikiFN($id); 302 303 // Get data(in instructions format) from $file (dont use cache: false) 304 305 $instr = p_cached_instructions($file, false); 306 307 // Page was not empty 308 309 if (!empty($instr)) { 310 311 // Fix relative links and lower headers of included pages 312 313 //we dont need this option for conversion 314 315 unset($o['options']['relative']); 316 317 $instr = $this->_convertInstructions( 318 $instr, 319 $id, 320 $renderer, 321 $clevel, 322 $o 323 ); 324 325 $info = array(); 326 327 // Render page 328 329 $content = p_render('xhtml', $instr, $info); 330 331 // Remove TOC`s, section edit buttons and tags 332 333 $content = $this->_cleanXHTML($content); 334 } 335 336 // Embed the included page 337 338 $renderer->doc .= '<div class="include">'; 339 340 // Add an anchor to find start of a inserted page 341 342 $id = str_replace(":", "_", $file_data['id']); 343 $renderer->doc .= "<a name='$id' id='$id'>"; 344 $renderer->doc .= $content; 345 $renderer->doc .= '</div>'; 346 347 return true; 348 349 } 350 351 /* 352 * Corrects relative internal links and media and 353 * converts headers of included pages to subheaders of the current page 354 */ 355 function _convertInstructions($instr, $incl, &$renderer, $clevel, $o){ 356 357 global $ID; 358 359 // check if included page is already in output namespace 360 $iNS = getNS($incl); 361 $iID = getNS($ID); 362 363 //the content belongs to the original page 364 if ($iID == $iNS) { 365 $convert = false; //just leave as it is 366 } else { 367 // The content was newly included, and is not original page content 368 $convert = true; //convert content 369 } 370 371 for ($i = 0; $i < count($instr); $i++){ 372 373 if ($convert) { 374 375 if((substr($instr[$i][0], 0, 8) == 'internal')){ 376 377 // Internal links(links inside this wiki) an relative links 378 379 $this->_convert_link($renderer,$instr[$i],$iNS,$iID,$o); 380 381 } elseif ($instr[$i][0] == 'header'){ 382 383 // Set header level to current section level + header level 384 385 $this->_convert_header($renderer,$instr[$i],$clevel,$o); 386 387 } elseif ($instr[$i][0] == 'section_open'){ 388 389 // The same for sections 390 391 $level = $instr[$i][1][0] + $clevel; 392 393 if ($level > 5) { 394 395 $level = 5; 396 397 } 398 399 $instr[$i][1][0] = $level; 400 401 } 402 } 403 404 // Split long lines? 405 406 if($o['split_too_long'] && $instr[$i][0] == 'code') { 407 408 $instr[$i][1][0] = wordwrap($instr[$i][1][0],70,"\n"); 409 410 } 411 412 } 413 414 // If its the document start, cut off the document information 415 416 if ($instr[0][0] == 'document_start') { 417 418 return array_slice($instr, 1, -1); 419 420 } else { 421 422 return $instr; 423 424 } 425 426 } 427 428 /* 429 * convert header of given instruction 430 */ 431 function _convert_header(&$renderer,&$instr,$clevel,$o) { 432 433 global $conf; 434 435 $level = $instr[1][1] + $clevel; 436 437 // If a header level gets "lower" than 5 438 if ($level > 5) { 439 440 $level = 5; 441 442 } 443 444 $instr[1][1] = $level; 445 446 // Number headers 447 448 if($o['numbers']) { 449 450 // Number in front of header 451 $number = ""; 452 453 // Reset lower levels 454 for($x = $level+1; $x <= 5; $x++) { 455 $o['header_count'][$x] = 0; 456 } 457 458 // Raise this level by 1 459 $o['header_count'][$level]++; 460 461 // If the level is high (1=high) 462 463 if($level <= $o['numbering_depth']) { 464 //build the number 465 for($x = 1; $x <= $level; $x++) { 466 $number .= $o['header_count'][$x] . "."; 467 } 468 } 469 470 //save 471 $instr[1][0] = $number . " " . $instr[1][0]; 472 } 473 474 // add TOC items 475 if ($level >= $conf['toptoclevel'] && $level <= $conf['maxtoclevel']){ 476 477 $text = $instr[1][0]; 478 $header_id = $renderer->_headerToLink($text, 'true'); 479 480 //add item to TOC 481 $renderer->toc[] = array( 482 'hid' => $header_id, 483 'title' => $text, 484 'type' => 'ul', 485 'level' => $level-$conf['toptoclevel']+1, 486 ); 487 488 } 489 } 490 491 /* 492 * Convert link of given instruction 493 */ 494 function _convert_link(&$renderer, &$instr, $iNS, $id, $o) { 495 496 // Relative subnamespace 497 if ($instr[1][0]{0} == '.'){ 498 499 // Build complete address 500 $instr[1][0] = $iNS.':'.substr($instr[1][0], 1); 501 502 } else if (strpos($instr[1][0],':') === false) { 503 //relative link 504 505 $instr[1][0] = $iNS.':'.$instr[1][0]; 506 } 507 508 // link to another page, but within our included namespace 509 // if the link starts with our namespace 510 511 if(strpos($instr[1][0], $o['start_ns']) === 0) { 512 513 //if the link is inside the included depth of pages 514 if($o['depth'] === 0 || 515 substr_count($o['start_ns'],":") + $o['depth'] >= 516 substr_count($instr[1][0],":") 517 ) { 518 519 // Convert to internal link 520 if(strpos($instr[1][0],"#") !== false) { 521 // Get last part a:b#c -> c 522 $levels = preg_split("/#/",$instr[1][0]); 523 524 // $id we are in atm + # + id of anchor 525 $instr[1][0] = $id . "#" . $levels[sizeof($levels)-1]; 526 } else { 527 528 // a:b:c 529 530 /** 531 * $id we are in atm + # + full id of page to find(-> 532 * unique id) not possible to use a:b:c for id --> : -> _ 533 */ 534 535 $instr[1][0] = $id . "#" . str_replace( 536 ":", 537 "_", 538 $instr[1][0] 539 ); 540 541 // If page-link has no name 542 543 if(empty($instr[1][1])) { 544 545 // TODO: get the real name of the page :( 546 547 $name = preg_split("/_/",$instr[1][0]); 548 549 // Last part of id -> name 550 551 $instr[1][1] = $name[sizeof($name)-1]; 552 } 553 } 554 } 555 } 556 } 557 558 /** 559 * Remove TOC, section edit buttons and tags 560 */ 561 function _cleanXHTML($xhtml){ 562 563 $replace = array( 564 '!<div class="toc">.*?(</div>\n</div>)!s' => '', // remove TOCs 565 '#<!-- SECTION \[(\d*-\d*)\] -->#e' => '', // remove section edit buttons 566 '!<div id="tags">.*?(</div>)!s' => '' // remove category tags 567 ); 568 569 $xhtml = preg_replace( 570 array_keys($replace), 571 array_values($replace), 572 $xhtml 573 ); 574 575 return $xhtml; 576 } 577} 578 579/* 580* evaluate the files presented by search 581* 582* modify $data according to our wishes 583*/ 584function composer_search_index(&$data, $base, $file, $type, $lvl, $opts) { 585 586 // include in result ? 587 $return = true; 588 589 // Directories 590 if ($type == 'd'){ 591 //if max-level is reached, don't return them 592 if ($opts['level'] == $lvl) { 593 594 $return = false; 595 596 } 597 598 //add the directory name to list of blocked files 599 //so that file with same name will not be included 600 $data['blocked'][] = $file . ".txt"; 601 602 //if we don't want namespace-nodes in the resultset 603 if ($opts['nons']) return $return; 604 605 } elseif($type == 'f' && !preg_match('#\.txt$#',$file)){ 606 // Don't add files, that end in txt 607 return false; 608 } 609 610 $id = pathID($file); 611 612 // Check hiddens 613 if($type == 'f' && isHiddenPage($id)){ 614 return false; 615 } 616 617 // Check ACL (for namespaces too) 618 if(auth_quickaclcheck($id) < AUTH_READ){ 619 //we are not allowed to read 620 return false; 621 } 622 623 //check if this files was blocked by us 624 if($type == 'f' && in_array($file,$data['blocked'])){ 625 return false; 626 } 627 628 // add the start level(ns we build) to the current level 629 $lvl += $data['start_lvl']; 630 631 //pack it up 632 $data[]=array( 'id' => $id, 633 'type' => $type, //which type is it ? 634 'level' => $lvl, //show on which level ? 635 'open' => $return, //open it ? 636 ); 637 638 return $return; 639 640} 641 642/** 643 * User defined sort function to sort by level, than by directory, 644 * than by start.txt and then by filename 645 * 646 * @param $a Array left side sort argument 647 * @param $b Array right side sort argument 648 */ 649 650function composer_sort_filedata($a, $b) { 651 652 global $conf; 653 654 // Sort by level 655 656 if ($a["level"] < $b["level"]) { 657 658 return -1; 659 660 } else if ($a["level"] > $b["level"]) { 661 662 return 1; 663 664 } else { 665 666 // Level is the same. Sort by directories 667 668 if (($a["type"] == "d") and ($b["type"] == "f")) { 669 670 return -1; 671 672 } else if (($a["type"] == "f") and ($b["type"] == "d")) { 673 674 return 1; 675 676 } else if (($a["type"] == "d") and ($b["type"] == "d")) { 677 678 // Both are directories. Sort by id 679 680 return strcmp($a["id"], $b["id"]); 681 682 } else { 683 684 // Both are files. Sort by start.txt 685 686 if (stristr($a["id"], $conf["start"]) && 687 !stristr($b["id"], $conf["start"]) 688 ) { 689 690 return -1; 691 692 } else if (!stristr($a["id"], $conf["start"]) && 693 stristr($b["id"], $conf["start"]) 694 ) { 695 696 return 1; 697 698 } else { 699 700 // No page is a start page or both, sort by id 701 702 return strcmp($a["id"], $b["id"]); 703 704 } 705 706 } 707 } 708 709}