1<?php 2if(!defined('DOKU_INC')) define('DOKU_INC',realpath(dirname(__FILE__).'/../../').'/'); 3 4class Doku_Handler { 5 6 var $Renderer = NULL; 7 8 var $CallWriter = NULL; 9 10 var $calls = array(); 11 12 var $meta = array( 13 'section' => FALSE, 14 'toc' => TRUE, 15 ); 16 17 var $rewriteBlocks = TRUE; 18 19 function Doku_Handler() { 20 $this->CallWriter = & new Doku_Handler_CallWriter($this); 21 } 22 23 function _addCall($handler, $args, $pos) { 24 $call = array($handler,$args, $pos); 25 $this->CallWriter->writeCall($call); 26 } 27 28 function _finalize(){ 29 if ( $this->meta['section'] ) { 30 $S = & new Doku_Handler_Section(); 31 $this->calls = $S->process($this->calls); 32 } 33 34 if ( $this->rewriteBlocks ) { 35 $B = & new Doku_Handler_Block(); 36 $this->calls = $B->process($this->calls); 37 } 38 39 if ( $this->meta['toc'] ) { 40 $T = & new Doku_Handler_Toc(); 41 $this->calls = $T->process($this->calls); 42 } 43 44 array_unshift($this->calls,array('document_start',array(),0)); 45 $last_call = end($this->calls); 46 array_push($this->calls,array('document_end',array(),$last_call[2])); 47 } 48 49 function fetch() { 50 $call = each($this->calls); 51 if ( $call ) { 52 return $call['value']; 53 } 54 return FALSE; 55 } 56 57 function base($match, $state, $pos) { 58 switch ( $state ) { 59 case DOKU_LEXER_UNMATCHED: 60 $this->_addCall('cdata',array($match), $pos); 61 return TRUE; 62 break; 63 64 } 65 } 66 67 function header($match, $state, $pos) { 68 $match = trim($match); 69 $levels = array( 70 '======'=>1, 71 '====='=>2, 72 '===='=>3, 73 '==='=>4, 74 '=='=>5, 75 ); 76 $hsplit = preg_split( '/(={2,})/u', $match,-1, 77 PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); 78 79 // Locate the level - default to level 1 if no match (title contains == signs) 80 if ( isset($hsplit[0]) && array_key_exists($hsplit[0], $levels) ) { 81 $level = $levels[$hsplit[0]]; 82 } else { 83 $level = 1; 84 } 85 86 // Strip of the marker for the header, based on the level - the rest is the title 87 $iLevels = array_flip($levels); 88 $markerLen = strlen($iLevels[$level]); 89 $title = substr($match, $markerLen, strlen($match)-($markerLen*2)); 90 91 $this->_addCall('header',array($title,$level,$pos), $pos); 92 $this->meta['section'] = TRUE; 93 return TRUE; 94 } 95 96 function notoc($match, $state, $pos) { 97 $this->meta['toc'] = FALSE; 98 return TRUE; 99 } 100 101 function linebreak($match, $state, $pos) { 102 $this->_addCall('linebreak',array(),$pos); 103 return TRUE; 104 } 105 106 function eol($match, $state, $pos) { 107 $this->_addCall('eol',array(),$pos); 108 return TRUE; 109 } 110 111 function hr($match, $state, $pos) { 112 $this->_addCall('hr',array(),$pos); 113 return TRUE; 114 } 115 116 function _nestingTag($match, $state, $pos, $name) { 117 switch ( $state ) { 118 case DOKU_LEXER_ENTER: 119 $this->_addCall($name.'_open', array(), $pos); 120 break; 121 case DOKU_LEXER_EXIT: 122 $this->_addCall($name.'_close', array(), $pos); 123 break; 124 case DOKU_LEXER_UNMATCHED: 125 $this->_addCall('cdata',array($match), $pos); 126 break; 127 } 128 } 129 130 function strong($match, $state, $pos) { 131 $this->_nestingTag($match, $state, $pos, 'strong'); 132 return TRUE; 133 } 134 135 function emphasis($match, $state, $pos) { 136 $this->_nestingTag($match, $state, $pos, 'emphasis'); 137 return TRUE; 138 } 139 140 function underline($match, $state, $pos) { 141 $this->_nestingTag($match, $state, $pos, 'underline'); 142 return TRUE; 143 } 144 145 function monospace($match, $state, $pos) { 146 $this->_nestingTag($match, $state, $pos, 'monospace'); 147 return TRUE; 148 } 149 150 function subscript($match, $state, $pos) { 151 $this->_nestingTag($match, $state, $pos, 'subscript'); 152 return TRUE; 153 } 154 155 function superscript($match, $state, $pos) { 156 $this->_nestingTag($match, $state, $pos, 'superscript'); 157 return TRUE; 158 } 159 160 function deleted($match, $state, $pos) { 161 $this->_nestingTag($match, $state, $pos, 'deleted'); 162 return TRUE; 163 } 164 165 166 function footnote($match, $state, $pos) { 167 $this->_nestingTag($match, $state, $pos, 'footnote'); 168 return TRUE; 169 } 170 171 function listblock($match, $state, $pos) { 172 switch ( $state ) { 173 case DOKU_LEXER_ENTER: 174 $ReWriter = & new Doku_Handler_List($this->CallWriter); 175 $this->CallWriter = & $ReWriter; 176 $this->_addCall('list_open', array($match), $pos); 177 break; 178 case DOKU_LEXER_EXIT: 179 $this->_addCall('list_close', array(), $pos); 180 $this->CallWriter->process(); 181 $ReWriter = & $this->CallWriter; 182 $this->CallWriter = & $ReWriter->CallWriter; 183 break; 184 case DOKU_LEXER_MATCHED: 185 $this->_addCall('list_item', array($match), $pos); 186 break; 187 case DOKU_LEXER_UNMATCHED: 188 $this->_addCall('cdata', array($match), $pos); 189 break; 190 } 191 return TRUE; 192 } 193 194 function unformatted($match, $state, $pos) { 195 if ( $state == DOKU_LEXER_UNMATCHED ) { 196 $this->_addCall('unformatted',array($match), $pos); 197 } 198 return TRUE; 199 } 200 201 function php($match, $state, $pos) { 202 if ( $state == DOKU_LEXER_UNMATCHED ) { 203 $this->_addCall('php',array($match), $pos); 204 } 205 return TRUE; 206 } 207 208 function html($match, $state, $pos) { 209 if ( $state == DOKU_LEXER_UNMATCHED ) { 210 $this->_addCall('html',array($match), $pos); 211 } 212 return TRUE; 213 } 214 215 function preformatted($match, $state, $pos) { 216 switch ( $state ) { 217 case DOKU_LEXER_ENTER: 218 $ReWriter = & new Doku_Handler_Preformatted($this->CallWriter); 219 $this->CallWriter = & $ReWriter; 220 $this->_addCall('preformatted_start',array(), $pos); 221 break; 222 case DOKU_LEXER_EXIT: 223 $this->_addCall('preformatted_end',array(), $pos); 224 $this->CallWriter->process(); 225 $ReWriter = & $this->CallWriter; 226 $this->CallWriter = & $ReWriter->CallWriter; 227 break; 228 case DOKU_LEXER_MATCHED: 229 $this->_addCall('preformatted_newline',array(), $pos); 230 break; 231 case DOKU_LEXER_UNMATCHED: 232 $this->_addCall('preformatted_content',array($match), $pos); 233 break; 234 } 235 236 return TRUE; 237 } 238 239 function file($match, $state, $pos) { 240 if ( $state == DOKU_LEXER_UNMATCHED ) { 241 $this->_addCall('file',array($match), $pos); 242 } 243 return TRUE; 244 } 245 246 function quote($match, $state, $pos) { 247 248 switch ( $state ) { 249 250 case DOKU_LEXER_ENTER: 251 $ReWriter = & new Doku_Handler_Quote($this->CallWriter); 252 $this->CallWriter = & $ReWriter; 253 $this->_addCall('quote_start',array($match), $pos); 254 break; 255 256 case DOKU_LEXER_EXIT: 257 $this->_addCall('quote_end',array(), $pos); 258 $this->CallWriter->process(); 259 $ReWriter = & $this->CallWriter; 260 $this->CallWriter = & $ReWriter->CallWriter; 261 break; 262 263 case DOKU_LEXER_MATCHED: 264 $this->_addCall('quote_newline',array($match), $pos); 265 break; 266 267 case DOKU_LEXER_UNMATCHED: 268 $this->_addCall('cdata',array($match), $pos); 269 break; 270 271 } 272 273 return TRUE; 274 } 275 276 function code($match, $state, $pos) { 277 switch ( $state ) { 278 case DOKU_LEXER_UNMATCHED: 279 $matches = preg_split('/>/u',$match,2); 280 $matches[0] = trim($matches[0]); 281 if ( trim($matches[0]) == '' ) { 282 $matches[0] = NULL; 283 } 284 # $matches[0] contains name of programming language 285 # if available 286 $this->_addCall( 287 'code', 288 array($matches[1],$matches[0]), 289 $pos 290 ); 291 break; 292 } 293 return TRUE; 294 } 295 296 function acronym($match, $state, $pos) { 297 $this->_addCall('acronym',array($match), $pos); 298 return TRUE; 299 } 300 301 function smiley($match, $state, $pos) { 302 $this->_addCall('smiley',array($match), $pos); 303 return TRUE; 304 } 305 306 function wordblock($match, $state, $pos) { 307 $this->_addCall('wordblock',array($match), $pos); 308 return TRUE; 309 } 310 311 function entity($match, $state, $pos) { 312 $this->_addCall('entity',array($match), $pos); 313 return TRUE; 314 } 315 316 function multiplyentity($match, $state, $pos) { 317 preg_match_all('/\d+/',$match,$matches); 318 $this->_addCall('multiplyentity',array($matches[0][0],$matches[0][1]), $pos); 319 return TRUE; 320 } 321 322 function singlequoteopening($match, $state, $pos) { 323 $this->_addCall('singlequoteopening',array(), $pos); 324 return TRUE; 325 } 326 327 function singlequoteclosing($match, $state, $pos) { 328 $this->_addCall('singlequoteclosing',array(), $pos); 329 return TRUE; 330 } 331 332 function doublequoteopening($match, $state, $pos) { 333 $this->_addCall('doublequoteopening',array(), $pos); 334 return TRUE; 335 } 336 337 function doublequoteclosing($match, $state, $pos) { 338 $this->_addCall('doublequoteclosing',array(), $pos); 339 return TRUE; 340 } 341 342 function camelcaselink($match, $state, $pos) { 343 $this->_addCall('camelcaselink',array($match), $pos); 344 return TRUE; 345 } 346 347 /* 348 */ 349 function internallink($match, $state, $pos) { 350 // Strip the opening and closing markup 351 $link = preg_replace(array('/^\[\[/','/\]\]$/u'),'',$match); 352 353 // Split title from URL 354 $link = preg_split('/\|/u',$link,2); 355 if ( !isset($link[1]) ) { 356 $link[1] = NULL; 357 } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) { 358 // If the title is an image, convert it to an array containing the image details 359 $link[1] = Doku_Handler_Parse_Media($link[1]); 360 } 361 362 //decide which kind of link it is 363 364 if ( preg_match('/^[a-zA-Z]+>{1}.+$/u',$link[0]) ) { 365 // Interwiki 366 $interwiki = preg_split('/>/u',$link[0]); 367 $this->_addCall( 368 'interwikilink', 369 array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]), 370 $pos 371 ); 372 }elseif ( preg_match('/\\\\\\\\[\w.:?\-;,]+?\\\\/u',$link[0]) ) { 373 // Windows Share 374 $this->_addCall( 375 'windowssharelink', 376 array($link[0],$link[1]), 377 $pos 378 ); 379 }elseif ( preg_match('#([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i',$link[0]) ) { 380 // E-Mail 381 $this->_addCall( 382 'emaillink', 383 array($link[0],$link[1]), 384 $pos 385 ); 386 }elseif ( preg_match('#^([a-z0-9]+?)://#i',$link[0]) ) { 387 // external link (accepts all protocols) 388 $this->_addCall( 389 'externallink', 390 array($link[0],$link[1]), 391 $pos 392 ); 393 }else{ 394 // internal link 395 $this->_addCall( 396 'internallink', 397 array($link[0],$link[1]), 398 $pos 399 ); 400 } 401 402 return TRUE; 403 } 404 405 function filelink($match, $state, $pos) { 406 $this->_addCall('filelink',array($match, NULL), $pos); 407 return TRUE; 408 } 409 410 function windowssharelink($match, $state, $pos) { 411 $this->_addCall('windowssharelink',array($match, NULL), $pos); 412 return TRUE; 413 } 414 415 function media($match, $state, $pos) { 416 $p = Doku_Handler_Parse_Media($match); 417 418 $this->_addCall( 419 $p['type'], 420 array($p['src'], $p['title'], $p['align'], $p['width'], $p['height'], $p['cache']), 421 $pos 422 ); 423 return TRUE; 424 } 425 426 function rss($match, $state, $pos) { 427 $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match); 428 $this->_addCall('rss',array($link),$pos); 429 } 430 431 function externallink($match, $state, $pos) { 432 // Prevent use of multibyte strings in URLs 433 // See: http://www.boingboing.net/2005/02/06/shmoo_group_exploit_.html 434 // Not worried about other charsets so long as page is output as UTF-8 435 /*if ( strlen($match) != utf8_strlen($match) ) { 436 $this->_addCall('cdata',array($match), $pos); 437 } else {*/ 438 439 $this->_addCall('externallink',array($match, NULL), $pos); 440 //} 441 return TRUE; 442 } 443 444 function emaillink($match, $state, $pos) { 445 $email = preg_replace(array('/^</','/>$/'),'',$match); 446 $this->_addCall('emaillink',array($email, NULL), $pos); 447 return TRUE; 448 } 449 450 function table($match, $state, $pos) { 451 switch ( $state ) { 452 453 case DOKU_LEXER_ENTER: 454 455 $ReWriter = & new Doku_Handler_Table($this->CallWriter); 456 $this->CallWriter = & $ReWriter; 457 458 $this->_addCall('table_start', array(), $pos); 459 //$this->_addCall('table_row', array(), $pos); 460 if ( trim($match) == '^' ) { 461 $this->_addCall('tableheader', array(), $pos); 462 } else { 463 $this->_addCall('tablecell', array(), $pos); 464 } 465 break; 466 467 case DOKU_LEXER_EXIT: 468 $this->_addCall('table_end', array(), $pos); 469 $this->CallWriter->process(); 470 $ReWriter = & $this->CallWriter; 471 $this->CallWriter = & $ReWriter->CallWriter; 472 break; 473 474 case DOKU_LEXER_UNMATCHED: 475 if ( trim($match) != '' ) { 476 $this->_addCall('cdata',array($match), $pos); 477 } 478 break; 479 480 case DOKU_LEXER_MATCHED: 481 if ( preg_match('/.{2}/',$match) ) { 482 $this->_addCall('table_align', array($match), $pos); 483 } else if ( $match == "\n|" ) { 484 $this->_addCall('table_row', array(), $pos); 485 $this->_addCall('tablecell', array(), $pos); 486 } else if ( $match == "\n^" ) { 487 $this->_addCall('table_row', array(), $pos); 488 $this->_addCall('tableheader', array(), $pos); 489 } else if ( $match == '|' ) { 490 $this->_addCall('tablecell', array(), $pos); 491 } else if ( $match == '^' ) { 492 $this->_addCall('tableheader', array(), $pos); 493 } 494 break; 495 } 496 return TRUE; 497 } 498} 499 500//------------------------------------------------------------------------ 501function Doku_Handler_Parse_Media($match) { 502 503 // Strip the opening and closing markup 504 $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match); 505 506 // Split title from URL 507 $link = preg_split('/\|/u',$link,2); 508 509 510 // Check alignment 511 $ralign = (bool)preg_match('/^ /',$link[0]); 512 $lalign = (bool)preg_match('/ $/',$link[0]); 513 514 // Logic = what's that ;)... 515 if ( $lalign & $ralign ) { 516 $align = 'center'; 517 } else if ( $ralign ) { 518 $align = 'right'; 519 } else if ( $lalign ) { 520 $align = 'left'; 521 } else { 522 $align = NULL; 523 } 524 525 // The title... 526 if ( !isset($link[1]) ) { 527 $link[1] = NULL; 528 } 529 530 //remove aligning spaces 531 $link[0] = trim($link[0]); 532 533 //split into src and parameters (using the very last questionmark) 534 $pos = strrpos($link[0], '?'); 535 if($pos !== false){ 536 $src = substr($link[0],0,$pos); 537 $param = substr($link[0],$pos+1); 538 }else{ 539 $src = $link[0]; 540 $param = ''; 541 } 542 543 //parse width and height 544 if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){ 545 ($size[1]) ? $w = $size[1] : $w = NULL; 546 ($size[3]) ? $h = $size[3] : $h = NULL; 547 } 548 549 //get caching command 550 if (preg_match('/(nocache|recache)/i',$param,$cachemode)){ 551 $cache = $cachemode[1]; 552 }else{ 553 $cache = 'cache'; 554 } 555 556 // Check whether this is a local or remote image 557 if ( preg_match('#^(https?|ftp)#i',$src) ) { 558 $call = 'externalmedia'; 559 } else { 560 $call = 'internalmedia'; 561 } 562 563 $params = array( 564 'type'=>$call, 565 'src'=>$src, 566 'title'=>$link[1], 567 'align'=>$align, 568 'width'=>$w, 569 'height'=>$h, 570 'cache'=>$cache, 571 ); 572 573 return $params; 574} 575 576//------------------------------------------------------------------------ 577class Doku_Handler_CallWriter { 578 579 var $Handler; 580 581 function Doku_Handler_CallWriter(& $Handler) { 582 $this->Handler = & $Handler; 583 } 584 585 function writeCall($call) { 586 $this->Handler->calls[] = $call; 587 } 588 589 function writeCalls($calls) { 590 $this->Handler->calls = array_merge($this->Handler->calls, $calls); 591 } 592} 593 594//------------------------------------------------------------------------ 595class Doku_Handler_List { 596 597 var $CallWriter; 598 599 var $calls = array(); 600 var $listCalls = array(); 601 var $listStack = array(); 602 603 function Doku_Handler_List(& $CallWriter) { 604 $this->CallWriter = & $CallWriter; 605 } 606 607 function writeCall($call) { 608 $this->calls[] = $call; 609 } 610 611 // Probably not needed but just in case... 612 function writeCalls($calls) { 613 $this->calls = array_merge($this->calls, $calls); 614 $this->CallWriter->writeCalls($this->calls); 615 } 616 617 //------------------------------------------------------------------------ 618 function process() { 619 foreach ( $this->calls as $call ) { 620 switch ($call[0]) { 621 case 'list_item': 622 $this->listOpen($call); 623 break; 624 case 'list_open': 625 $this->listStart($call); 626 break; 627 case 'list_close': 628 $this->listEnd($call); 629 break; 630 default: 631 $this->listContent($call); 632 break; 633 } 634 } 635 636 $this->CallWriter->writeCalls($this->listCalls); 637 } 638 639 //------------------------------------------------------------------------ 640 function listStart($call) { 641 $depth = $this->interpretSyntax($call[1][0], $listType); 642 643 $this->initialDepth = $depth; 644 $this->listStack[] = array($listType, $depth); 645 646 $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]); 647 $this->listCalls[] = array('listitem_open',array(1),$call[2]); 648 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 649 } 650 651 //------------------------------------------------------------------------ 652 function listEnd($call) { 653 $closeContent = TRUE; 654 655 while ( $list = array_pop($this->listStack) ) { 656 if ( $closeContent ) { 657 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 658 $closeContent = FALSE; 659 } 660 $this->listCalls[] = array('listitem_close',array(),$call[2]); 661 $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]); 662 } 663 } 664 665 //------------------------------------------------------------------------ 666 function listOpen($call) { 667 $depth = $this->interpretSyntax($call[1][0], $listType); 668 $end = end($this->listStack); 669 670 // Not allowed to be shallower than initialDepth 671 if ( $depth < $this->initialDepth ) { 672 $depth = $this->initialDepth; 673 } 674 675 //------------------------------------------------------------------------ 676 if ( $depth == $end[1] ) { 677 678 // Just another item in the list... 679 if ( $listType == $end[0] ) { 680 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 681 $this->listCalls[] = array('listitem_close',array(),$call[2]); 682 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 683 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 684 685 // Switched list type... 686 } else { 687 688 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 689 $this->listCalls[] = array('listitem_close',array(),$call[2]); 690 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 691 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 692 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 693 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 694 695 array_pop($this->listStack); 696 $this->listStack[] = array($listType, $depth); 697 } 698 699 //------------------------------------------------------------------------ 700 // Getting deeper... 701 } else if ( $depth > $end[1] ) { 702 703 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 704 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 705 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 706 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 707 708 $this->listStack[] = array($listType, $depth); 709 710 //------------------------------------------------------------------------ 711 // Getting shallower ( $depth < $end[1] ) 712 } else { 713 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 714 $this->listCalls[] = array('listitem_close',array(),$call[2]); 715 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 716 717 // Throw away the end - done 718 array_pop($this->listStack); 719 720 while (1) { 721 $end = end($this->listStack); 722 723 if ( $end[1] <= $depth ) { 724 725 // Normalize depths 726 $depth = $end[1]; 727 728 $this->listCalls[] = array('listitem_close',array(),$call[2]); 729 730 if ( $end[0] == $listType ) { 731 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 732 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 733 734 } else { 735 // Switching list type... 736 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 737 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 738 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 739 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 740 741 array_pop($this->listStack); 742 $this->listStack[] = array($listType, $depth); 743 } 744 745 break; 746 747 // Haven't dropped down far enough yet.... ( $end[1] > $depth ) 748 } else { 749 750 $this->listCalls[] = array('listitem_close',array(),$call[2]); 751 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 752 753 array_pop($this->listStack); 754 755 } 756 757 } 758 759 } 760 } 761 762 //------------------------------------------------------------------------ 763 function listContent($call) { 764 $this->listCalls[] = $call; 765 } 766 767 //------------------------------------------------------------------------ 768 function interpretSyntax($match, & $type) { 769 if ( substr($match,-1) == '*' ) { 770 $type = 'u'; 771 } else { 772 $type = 'o'; 773 } 774 return count(explode(' ',str_replace("\t",' ',$match))); 775 } 776} 777 778//------------------------------------------------------------------------ 779class Doku_Handler_Preformatted { 780 781 var $CallWriter; 782 783 var $calls = array(); 784 var $pos; 785 var $text =''; 786 787 788 789 function Doku_Handler_Preformatted(& $CallWriter) { 790 $this->CallWriter = & $CallWriter; 791 } 792 793 function writeCall($call) { 794 $this->calls[] = $call; 795 } 796 797 // Probably not needed but just in case... 798 function writeCalls($calls) { 799 $this->calls = array_merge($this->calls, $calls); 800 $this->CallWriter->writeCalls($this->calls); 801 } 802 803 function process() { 804 foreach ( $this->calls as $call ) { 805 switch ($call[0]) { 806 case 'preformatted_start': 807 $this->pos = $call[2]; 808 break; 809 case 'preformatted_newline': 810 $this->text .= "\n"; 811 break; 812 case 'preformatted_content': 813 $this->text .= $call[1][0]; 814 break; 815 case 'preformatted_end': 816 $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos)); 817 break; 818 } 819 } 820 } 821} 822 823//------------------------------------------------------------------------ 824class Doku_Handler_Quote { 825 826 var $CallWriter; 827 828 var $calls = array(); 829 830 var $quoteCalls = array(); 831 832 function Doku_Handler_Quote(& $CallWriter) { 833 $this->CallWriter = & $CallWriter; 834 } 835 836 function writeCall($call) { 837 $this->calls[] = $call; 838 } 839 840 // Probably not needed but just in case... 841 function writeCalls($calls) { 842 $this->calls = array_merge($this->calls, $calls); 843 $this->CallWriter->writeCalls($this->calls); 844 } 845 846 function process() { 847 848 $quoteDepth = 1; 849 850 foreach ( $this->calls as $call ) { 851 switch ($call[0]) { 852 853 case 'quote_start': 854 855 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 856 857 case 'quote_newline': 858 859 $quoteLength = $this->getDepth($call[1][0]); 860 861 if ( $quoteLength > $quoteDepth ) { 862 $quoteDiff = $quoteLength - $quoteDepth; 863 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 864 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 865 } 866 } else if ( $quoteLength < $quoteDepth ) { 867 $quoteDiff = $quoteDepth - $quoteLength; 868 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 869 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 870 } 871 } 872 873 $quoteDepth = $quoteLength; 874 875 break; 876 877 case 'quote_end': 878 879 if ( $quoteDepth > 1 ) { 880 $quoteDiff = $quoteDepth - 1; 881 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 882 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 883 } 884 } 885 886 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 887 888 $this->CallWriter->writeCalls($this->quoteCalls); 889 break; 890 891 default: 892 $this->quoteCalls[] = $call; 893 break; 894 } 895 } 896 } 897 898 function getDepth($marker) { 899 preg_match('/>{1,}/', $marker, $matches); 900 $quoteLength = strlen($matches[0]); 901 return $quoteLength; 902 } 903} 904 905//------------------------------------------------------------------------ 906class Doku_Handler_Table { 907 908 var $CallWriter; 909 910 var $calls = array(); 911 var $tableCalls = array(); 912 var $maxCols = 0; 913 var $maxRows = 1; 914 var $currentCols = 0; 915 var $firstCell = FALSE; 916 var $lastCellType = 'tablecell'; 917 918 function Doku_Handler_Table(& $CallWriter) { 919 $this->CallWriter = & $CallWriter; 920 } 921 922 function writeCall($call) { 923 $this->calls[] = $call; 924 } 925 926 // Probably not needed but just in case... 927 function writeCalls($calls) { 928 $this->calls = array_merge($this->calls, $calls); 929 $this->CallWriter->writeCalls($this->calls); 930 } 931 932 //------------------------------------------------------------------------ 933 function process() { 934 foreach ( $this->calls as $call ) { 935 switch ( $call[0] ) { 936 case 'table_start': 937 $this->tableStart($call); 938 break; 939 case 'table_row': 940 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 941 $this->tableRowOpen(array('tablerow_open',$call[1],$call[2])); 942 break; 943 case 'tableheader': 944 case 'tablecell': 945 $this->tableCell($call); 946 break; 947 case 'table_end': 948 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 949 $this->tableEnd($call); 950 break; 951 default: 952 $this->tableDefault($call); 953 break; 954 } 955 } 956 $this->CallWriter->writeCalls($this->tableCalls); 957 } 958 959 function tableStart($call) { 960 $this->tableCalls[] = array('table_open',array(),$call[2]); 961 $this->tableCalls[] = array('tablerow_open',array(),$call[2]); 962 $this->firstCell = TRUE; 963 } 964 965 function tableEnd($call) { 966 $this->tableCalls[] = array('table_close',array(),$call[2]); 967 $this->finalizeTable(); 968 } 969 970 function tableRowOpen($call) { 971 $this->tableCalls[] = $call; 972 $this->currentCols = 0; 973 $this->firstCell = TRUE; 974 $this->lastCellType = 'tablecell'; 975 $this->maxRows++; 976 } 977 978 function tableRowClose($call) { 979 // Strip off final cell opening and anything after it 980 while ( $discard = array_pop($this->tableCalls ) ) { 981 982 if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') { 983 984 // Its a spanning element - put it back and close it 985 if ( $discard[1][0] > 1 ) { 986 987 $this->tableCalls[] = $discard; 988 if ( strstr($discard[0],'cell') ) { 989 $name = 'tablecell'; 990 } else { 991 $name = 'tableheader'; 992 } 993 $this->tableCalls[] = array($name.'_close',array(),$call[2]); 994 } 995 996 break; 997 } 998 } 999 $this->tableCalls[] = $call; 1000 1001 if ( $this->currentCols > $this->maxCols ) { 1002 $this->maxCols = $this->currentCols; 1003 } 1004 } 1005 1006 function tableCell($call) { 1007 if ( !$this->firstCell ) { 1008 1009 // Increase the span 1010 $lastCall = end($this->tableCalls); 1011 1012 // A cell call which follows an open cell means an empty cell so span 1013 if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) { 1014 $this->tableCalls[] = array('colspan',array(),$call[2]); 1015 1016 } 1017 1018 $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]); 1019 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1020 $this->lastCellType = $call[0]; 1021 1022 } else { 1023 1024 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1025 $this->lastCellType = $call[0]; 1026 $this->firstCell = FALSE; 1027 1028 } 1029 1030 $this->currentCols++; 1031 } 1032 1033 function tableDefault($call) { 1034 $this->tableCalls[] = $call; 1035 } 1036 1037 function finalizeTable() { 1038 1039 // Add the max cols and rows to the table opening 1040 if ( $this->tableCalls[0][0] == 'table_open' ) { 1041 // Adjust to num cols not num col delimeters 1042 $this->tableCalls[0][1][] = $this->maxCols - 1; 1043 $this->tableCalls[0][1][] = $this->maxRows; 1044 } else { 1045 trigger_error('First element in table call list is not table_open'); 1046 } 1047 1048 $lastRow = 0; 1049 $lastCell = 0; 1050 $toDelete = array(); 1051 1052 // Look for the colspan elements and increment the colspan on the 1053 // previous non-empty opening cell. Once done, delete all the cells 1054 // that contain colspans 1055 foreach ( $this->tableCalls as $key => $call ) { 1056 1057 if ( $call[0] == 'tablerow_open' ) { 1058 1059 $lastRow = $key; 1060 1061 } else if ( $call[0] == 'tablecell_open' || $call[0] == 'tableheader_open' ) { 1062 1063 $lastCell = $key; 1064 1065 } else if ( $call[0] == 'table_align' ) { 1066 1067 // If the previous element was a cell open, align right 1068 if ( $this->tableCalls[$key-1][0] == 'tablecell_open' || $this->tableCalls[$key-1][0] == 'tableheader_open' ) { 1069 $this->tableCalls[$key-1][1][1] = 'right'; 1070 1071 // If the next element if the close of an element, align either center or left 1072 } else if ( $this->tableCalls[$key+1][0] == 'tablecell_close' || $this->tableCalls[$key+1][0] == 'tableheader_close' ) { 1073 if ( $this->tableCalls[$lastCell][1][1] == 'right' ) { 1074 $this->tableCalls[$lastCell][1][1] = 'center'; 1075 } else { 1076 $this->tableCalls[$lastCell][1][1] = 'left'; 1077 } 1078 1079 } 1080 1081 // Now convert the whitespace back to cdata 1082 $this->tableCalls[$key][0] = 'cdata'; 1083 1084 } else if ( $call[0] == 'colspan' ) { 1085 1086 $this->tableCalls[$key-1][1][0] = FALSE; 1087 1088 for($i = $key-2; $i > $lastRow; $i--) { 1089 1090 if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) { 1091 1092 if ( FALSE !== $this->tableCalls[$i][1][0] ) { 1093 $this->tableCalls[$i][1][0]++; 1094 break; 1095 } 1096 1097 1098 } 1099 } 1100 1101 $toDelete[] = $key-1; 1102 $toDelete[] = $key; 1103 $toDelete[] = $key+1; 1104 } 1105 } 1106 1107 foreach ( $toDelete as $delete ) { 1108 unset($this->tableCalls[$delete]); 1109 } 1110 1111 $this->tableCalls = array_values($this->tableCalls); 1112 } 1113} 1114 1115//------------------------------------------------------------------------ 1116class Doku_Handler_Section { 1117 1118 function process($calls) { 1119 1120 $sectionCalls = array(); 1121 $inSection = FALSE; 1122 1123 foreach ( $calls as $call ) { 1124 1125 if ( $call[0] == 'header' ) { 1126 1127 if ( $inSection ) { 1128 $sectionCalls[] = array('section_close',array(), $call[2]); 1129 } 1130 1131 $sectionCalls[] = $call; 1132 $sectionCalls[] = array('section_open',array($call[1][1]), $call[2]); 1133 $inSection = TRUE; 1134 1135 } else { 1136 $sectionCalls[] = $call; 1137 } 1138 } 1139 1140 if ( $inSection ) { 1141 $sectionCalls[] = array('section_close',array(), $call[2]); 1142 } 1143 1144 return $sectionCalls; 1145 } 1146 1147} 1148 1149//------------------------------------------------------------------------ 1150class Doku_Handler_Block { 1151 1152 var $calls = array(); 1153 1154 var $blockStack = array(); 1155 1156 var $inParagraph = FALSE; 1157 var $atStart = TRUE; 1158 var $skipEolKey = -1; 1159 1160 // Blocks don't contain linefeeds 1161 var $blockOpen = array( 1162 'header', 1163 'listu_open','listo_open','listitem_open', 1164 'table_open','tablerow_open','tablecell_open','tableheader_open', 1165 'quote_open', 1166 'section_open', // Needed to prevent p_open between header and section_open 1167 'code','file','php','html','hr','preformatted', 1168 ); 1169 1170 var $blockClose = array( 1171 'header', 1172 'listu_close','listo_close','listitem_close', 1173 'table_close','tablerow_close','tablecell_close','tableheader_close', 1174 'quote_close', 1175 'section_close', // Needed to prevent p_close after section_close 1176 'code','file','php','html','hr','preformatted', 1177 ); 1178 1179 // Stacks can contain linefeeds 1180 var $stackOpen = array( 1181 'footnote_open','section_open', 1182 ); 1183 1184 var $stackClose = array( 1185 'footnote_close','section_close', 1186 ); 1187 1188 function closeParagraph($pos){ 1189 // look back if there was any content - we don't want empty paragraphs 1190 $content = ''; 1191 for($i=count($this->calls)-1; $i>=0; $i--){ 1192 if($this->calls[$i][0] == 'p_open'){ 1193 break; 1194 }elseif($this->calls[$i][0] == 'cdata'){ 1195 $content .= $this->calls[$i][1][0]; 1196 }else{ 1197 $content = 'found markup'; 1198 break; 1199 } 1200 } 1201 1202 if(trim($content)==''){ 1203 //remove the whole paragraph 1204 array_splice($this->calls,$i); 1205 }else{ 1206 $this->calls[] = array('p_close',array(), $pos); 1207 } 1208 } 1209 1210 function process($calls) { 1211 foreach ( $calls as $key => $call ) { 1212 1213 // Process blocks which are stack like... (contain linefeeds) 1214 if ( in_array($call[0],$this->stackOpen ) ) { 1215 /* 1216 if ( $this->atStart ) { 1217 $this->calls[] = array('p_open',array(), $call[2]); 1218 $this->atStart = FALSE; 1219 $this->inParagraph = TRUE; 1220 } 1221 */ 1222 $this->calls[] = $call; 1223 1224 // Hack - footnotes shouldn't immediately contain a p_open 1225 if ( $call[0] != 'footnote_open' ) { 1226 $this->addToStack(); 1227 } else { 1228 $this->addToStack(FALSE); 1229 } 1230 continue; 1231 } 1232 1233 if ( in_array($call[0],$this->stackClose ) ) { 1234 1235 if ( $this->inParagraph ) { 1236 //$this->calls[] = array('p_close',array(), $call[2]); 1237 $this->closeParagraph($call[2]); 1238 } 1239 $this->calls[] = $call; 1240 $this->removeFromStack(); 1241 continue; 1242 } 1243 1244 if ( !$this->atStart ) { 1245 1246 if ( $call[0] == 'eol' ) { 1247 1248 1249 /* XXX 1250 if ( $this->inParagraph ) { 1251 $this->calls[] = array('p_close',array(), $call[2]); 1252 } 1253 $this->calls[] = array('p_open',array(), $call[2]); 1254 $this->inParagraph = TRUE; 1255 */ 1256 1257 # Check this isn't an eol instruction to skip... 1258 if ( $this->skipEolKey != $key ) { 1259 # Look to see if the next instruction is an EOL 1260 if ( isset($calls[$key+1]) && $calls[$key+1][0] == 'eol' ) { 1261 1262 if ( $this->inParagraph ) { 1263 //$this->calls[] = array('p_close',array(), $call[2]); 1264 $this->closeParagraph($call[2]); 1265 } 1266 1267 $this->calls[] = array('p_open',array(), $call[2]); 1268 $this->inParagraph = TRUE; 1269 1270 1271 # Mark the next instruction for skipping 1272 $this->skipEolKey = $key+1; 1273 1274 }else{ 1275 //if this is just a single eol make a space from it 1276 $this->calls[] = array('cdata',array(" "), $call[2]); 1277 } 1278 } 1279 1280 1281 } else { 1282 1283 $storeCall = TRUE; 1284 1285 if ( $this->inParagraph && in_array($call[0], $this->blockOpen) ) { 1286 //$this->calls[] = array('p_close',array(), $call[2]); 1287 $this->closeParagraph($call[2]); 1288 $this->inParagraph = FALSE; 1289 $this->calls[] = $call; 1290 $storeCall = FALSE; 1291 } 1292 1293 if ( in_array($call[0], $this->blockClose) ) { 1294 if ( $this->inParagraph ) { 1295 //$this->calls[] = array('p_close',array(), $call[2]); 1296 $this->closeParagraph($call[2]); 1297 $this->inParagraph = FALSE; 1298 } 1299 if ( $storeCall ) { 1300 $this->calls[] = $call; 1301 $storeCall = FALSE; 1302 } 1303 1304 // This really sucks and suggests this whole class sucks but... 1305 if ( isset($calls[$key+1]) 1306 && 1307 !in_array($calls[$key+1][0], $this->blockOpen) 1308 && 1309 !in_array($calls[$key+1][0], $this->blockClose) 1310 ) { 1311 1312 $this->calls[] = array('p_open',array(), $call[2]); 1313 $this->inParagraph = TRUE; 1314 } 1315 } 1316 1317 if ( $storeCall ) { 1318 $this->calls[] = $call; 1319 } 1320 1321 } 1322 1323 1324 } else { 1325 1326 // Unless there's already a block at the start, start a paragraph 1327 if ( !in_array($call[0],$this->blockOpen) ) { 1328 $this->calls[] = array('p_open',array(), $call[2]); 1329 if ( $call[0] != 'eol' ) { 1330 $this->calls[] = $call; 1331 } 1332 $this->atStart = FALSE; 1333 $this->inParagraph = TRUE; 1334 } else { 1335 $this->calls[] = $call; 1336 $this->atStart = FALSE; 1337 } 1338 1339 } 1340 1341 } 1342 1343 if ( $this->inParagraph ) { 1344 if ( $call[0] == 'p_open' ) { 1345 // Ditch the last call 1346 array_pop($this->calls); 1347 } else if ( !in_array($call[0], $this->blockClose) ) { 1348 //$this->calls[] = array('p_close',array(), $call[2]); 1349 $this->closeParagraph($call[2]); 1350 } else { 1351 $last_call = array_pop($this->calls); 1352 //$this->calls[] = array('p_close',array(), $call[2]); 1353 $this->closeParagraph($call[2]); 1354 $this->calls[] = $last_call; 1355 } 1356 } 1357 1358 return $this->calls; 1359 } 1360 1361 function addToStack($newStart = TRUE) { 1362 $this->blockStack[] = array($this->atStart, $this->inParagraph); 1363 $this->atStart = $newStart; 1364 $this->inParagraph = FALSE; 1365 } 1366 1367 function removeFromStack() { 1368 $state = array_pop($this->blockStack); 1369 $this->atStart = $state[0]; 1370 $this->inParagraph = $state[1]; 1371 } 1372} 1373//------------------------------------------------------------------------ 1374define('DOKU_TOC_OPEN',1); 1375define('DOKU_TOCBRANCH_OPEN',2); 1376define('DOKU_TOCITEM_OPEN',3); 1377define('DOKU_TOC_ELEMENT',4); 1378define('DOKU_TOCITEM_CLOSE',5); 1379define('DOKU_TOCBRANCH_CLOSE',6); 1380define('DOKU_TOC_CLOSE',7); 1381 1382class Doku_Handler_Toc { 1383 1384 var $calls = array(); 1385 var $tocStack = array(); 1386 var $toc = array(); 1387 var $numHeaders = 0; 1388 1389 function process($calls) { 1390 #FIXME can this be done better? 1391 global $conf; 1392 1393 foreach ( $calls as $call ) { 1394 if ( $call[0] == 'header' && $call[1][1] <= $conf['maxtoclevel'] ) { 1395 $this->numHeaders++; 1396 $this->addToToc($call); 1397 } 1398 $this->calls[] = $call; 1399 } 1400 1401 // Complete the table of contents then prepend to the calls 1402 $this->finalizeToc($call); 1403 return $this->calls; 1404 } 1405 1406 function addToToc($call) { 1407 1408 $depth = $call[1][1]; 1409 1410 // If it's the opening item... 1411 if ( count ( $this->toc) == 0 ) { 1412 1413 $this->addTocCall($call, DOKU_TOC_OPEN); 1414 1415 for ( $i = 1; $i <= $depth; $i++ ) { 1416 1417 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1418 1419 if ( $i != $depth ) { 1420 $this->addTocCall(array($call[0],array($call[1][0], $i, '', TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1421 } else { 1422 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1423 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOC_ELEMENT); 1424 } 1425 1426 $this->tocStack[] = $i; 1427 1428 } 1429 return; 1430 } 1431 1432 $currentDepth = end($this->tocStack); 1433 $initialDepth = $currentDepth; 1434 1435 // Create new branches as needed 1436 if ( $depth > $currentDepth ) { 1437 1438 for ($i = $currentDepth+1; $i <= $depth; $i++ ) { 1439 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1440 // It's just a filler 1441 if ( $i != $depth ) { 1442 $this->addTocCall(array($call[0],array($call[1][0], $i, '', TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1443 } else { 1444 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1445 } 1446 $this->tocStack[] = $i; 1447 } 1448 1449 $currentDepth = $i-1; 1450 1451 } 1452 1453 // Going down 1454 if ( $depth < $currentDepth ) { 1455 for ( $i = $currentDepth; $i >= $depth; $i-- ) { 1456 if ( $i != $depth ) { 1457 array_pop($this->tocStack); 1458 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1459 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_CLOSE); 1460 } else { 1461 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1462 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_OPEN); 1463 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1464 return; 1465 } 1466 } 1467 } 1468 1469 if ( $depth == $initialDepth ) { 1470 $this->addTocCall($call, DOKU_TOCITEM_CLOSE); 1471 $this->addTocCall($call, DOKU_TOCITEM_OPEN); 1472 } 1473 1474 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1475 1476 1477 } 1478 1479 function addTocCall($call, $type) { 1480 switch ( $type ) { 1481 case DOKU_TOC_OPEN: 1482 $this->toc[] = array('toc_open',array(),$call[2]); 1483 break; 1484 1485 case DOKU_TOCBRANCH_OPEN: 1486 $this->toc[] = array('tocbranch_open',array($call[1][1]),$call[2]); 1487 break; 1488 1489 case DOKU_TOCITEM_OPEN: 1490 if ( isset( $call[1][3] ) ) { 1491 $this->toc[] = array('tocitem_open',array($call[1][1], TRUE),$call[2]); 1492 } else { 1493 $this->toc[] = array('tocitem_open',array($call[1][1]),$call[2]); 1494 } 1495 break; 1496 1497 case DOKU_TOC_ELEMENT: 1498 $this->toc[] = array('tocelement',array($call[1][1],$call[1][0]),$call[2]); 1499 break; 1500 1501 case DOKU_TOCITEM_CLOSE: 1502 $this->toc[] = array('tocitem_close',array($call[1][1]),$call[2]); 1503 break; 1504 1505 case DOKU_TOCBRANCH_CLOSE: 1506 $this->toc[] = array('tocbranch_close',array($call[1][1]),$call[2]); 1507 break; 1508 1509 case DOKU_TOC_CLOSE: 1510 if ( count($this->toc) > 0 ) { 1511 $this->toc[] = array('toc_close',array(),$call[2]); 1512 } 1513 break; 1514 } 1515 } 1516 1517 function finalizeToc($call) { 1518 global $conf; 1519 if ( $this->numHeaders < $conf['maxtoclevel'] ) { 1520 return; 1521 } 1522 if ( count ($this->tocStack) > 0 ) { 1523 while ( NULL !== ($toc = array_pop($this->tocStack)) ) { 1524 $this->addTocCall(array($call[0],array('',$toc),$call[2]), DOKU_TOCITEM_CLOSE); 1525 $this->addTocCall(array($call[0],array('',$toc),$call[2]), DOKU_TOCBRANCH_CLOSE); 1526 } 1527 } 1528 $this->addTocCall($call, DOKU_TOC_CLOSE); 1529 $this->calls = array_merge($this->toc, $this->calls); 1530 } 1531 1532} 1533 1534 1535//Setup VIM: ex: et ts=4 enc=utf-8 : 1536