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 358 // If the title is an image, convert it to an array containing the image details 359 } else if ( preg_match('/^\{\{[^\}]+\}\}$/',$link[1]) ) { 360 $link[1] = Doku_Handler_Parse_Media($link[1]); 361 } 362 363 //decide which kind of link it is 364 365 if ( preg_match('/^[a-zA-Z]+>{1}[\w()\/\\#~:.?+=&%@!\-;,]+$/u',$link[0]) ) { 366 // Interwiki 367 $interwiki = preg_split('/>/u',$link[0]); 368 $this->_addCall( 369 'interwikilink', 370 array($link[0],$link[1],strtolower($interwiki[0]),$interwiki[1]), 371 $pos 372 ); 373 }elseif ( preg_match('/\\\\\\\\[\w.:?\-;,]+?\\\\/u',$link[0]) ) { 374 // Windows Share 375 $this->_addCall( 376 'windowssharelink', 377 array($link[0],$link[1]), 378 $pos 379 ); 380 }elseif ( preg_match('#([a-z0-9\-_.]+?)@([\w\-]+\.([\w\-\.]+\.)*[\w]+)#i',$link[0]) ) { 381 // E-Mail 382 $this->_addCall( 383 'emaillink', 384 array($link[0],$link[1]), 385 $pos 386 ); 387 }elseif ( preg_match('#^([a-z0-9]+?)://#i',$link[0]) ) { 388 // external link (accepts all protocols) 389 $this->_addCall( 390 'externallink', 391 array($link[0],$link[1]), 392 $pos 393 ); 394 }else{ 395 // internal link 396 $this->_addCall( 397 'internallink', 398 array($link[0],$link[1]), 399 $pos 400 ); 401 } 402 403 return TRUE; 404 } 405 406 function filelink($match, $state, $pos) { 407 $this->_addCall('filelink',array($match, NULL), $pos); 408 return TRUE; 409 } 410 411 function windowssharelink($match, $state, $pos) { 412 $this->_addCall('windowssharelink',array($match, NULL), $pos); 413 return TRUE; 414 } 415 416 function media($match, $state, $pos) { 417 $p = Doku_Handler_Parse_Media($match); 418 419 $this->_addCall( 420 $p['type'], 421 array($p['src'], $p['title'], $p['align'], $p['width'], $p['height'], $p['cache']), 422 $pos 423 ); 424 return TRUE; 425 } 426 427 function rss($match, $state, $pos) { 428 $link = preg_replace(array('/^\{\{rss>/','/\}\}$/'),'',$match); 429 $this->_addCall('rss',array($link),$pos); 430 } 431 432 function externallink($match, $state, $pos) { 433 // Prevent use of multibyte strings in URLs 434 // See: http://www.boingboing.net/2005/02/06/shmoo_group_exploit_.html 435 // Not worried about other charsets so long as page is output as UTF-8 436 /*if ( strlen($match) != utf8_strlen($match) ) { 437 $this->_addCall('cdata',array($match), $pos); 438 } else {*/ 439 440 $this->_addCall('externallink',array($match, NULL), $pos); 441 //} 442 return TRUE; 443 } 444 445 function emaillink($match, $state, $pos) { 446 $email = preg_replace(array('/^</','/>$/'),'',$match); 447 $this->_addCall('emaillink',array($email, NULL), $pos); 448 return TRUE; 449 } 450 451 function table($match, $state, $pos) { 452 switch ( $state ) { 453 454 case DOKU_LEXER_ENTER: 455 456 $ReWriter = & new Doku_Handler_Table($this->CallWriter); 457 $this->CallWriter = & $ReWriter; 458 459 $this->_addCall('table_start', array(), $pos); 460 //$this->_addCall('table_row', array(), $pos); 461 if ( trim($match) == '^' ) { 462 $this->_addCall('tableheader', array(), $pos); 463 } else { 464 $this->_addCall('tablecell', array(), $pos); 465 } 466 break; 467 468 case DOKU_LEXER_EXIT: 469 $this->_addCall('table_end', array(), $pos); 470 $this->CallWriter->process(); 471 $ReWriter = & $this->CallWriter; 472 $this->CallWriter = & $ReWriter->CallWriter; 473 break; 474 475 case DOKU_LEXER_UNMATCHED: 476 if ( trim($match) != '' ) { 477 $this->_addCall('cdata',array($match), $pos); 478 } 479 break; 480 481 case DOKU_LEXER_MATCHED: 482 if ( preg_match('/.{2}/',$match) ) { 483 $this->_addCall('table_align', array($match), $pos); 484 } else if ( $match == "\n|" ) { 485 $this->_addCall('table_row', array(), $pos); 486 $this->_addCall('tablecell', array(), $pos); 487 } else if ( $match == "\n^" ) { 488 $this->_addCall('table_row', array(), $pos); 489 $this->_addCall('tableheader', array(), $pos); 490 } else if ( $match == '|' ) { 491 $this->_addCall('tablecell', array(), $pos); 492 } else if ( $match == '^' ) { 493 $this->_addCall('tableheader', array(), $pos); 494 } 495 break; 496 } 497 return TRUE; 498 } 499} 500 501//------------------------------------------------------------------------ 502function Doku_Handler_Parse_Media($match) { 503 504 // Strip the opening and closing markup 505 $link = preg_replace(array('/^\{\{/','/\}\}$/u'),'',$match); 506 507 // Split title from URL 508 $link = preg_split('/\|/u',$link,2); 509 510 511 // Check alignment 512 $ralign = (bool)preg_match('/^ /',$link[0]); 513 $lalign = (bool)preg_match('/ $/',$link[0]); 514 515 // Logic = what's that ;)... 516 if ( $lalign & $ralign ) { 517 $align = 'center'; 518 } else if ( $ralign ) { 519 $align = 'right'; 520 } else if ( $lalign ) { 521 $align = 'left'; 522 } else { 523 $align = NULL; 524 } 525 526 // The title... 527 if ( !isset($link[1]) ) { 528 $link[1] = NULL; 529 } 530 531 //remove aligning spaces 532 $link[0] = trim($link[0]); 533 534 //split into src and parameters (using the very last questionmark) 535 $pos = strrpos($link[0], '?'); 536 if($pos !== false){ 537 $src = substr($link[0],0,$pos); 538 $param = substr($link[0],$pos+1); 539 }else{ 540 $src = $link[0]; 541 $param = ''; 542 } 543 544 //parse width and height 545 if(preg_match('#(\d+)(x(\d+))?#i',$param,$size)){ 546 ($size[1]) ? $w = $size[1] : $w = NULL; 547 ($size[3]) ? $h = $size[3] : $h = NULL; 548 } 549 550 //get caching command 551 if (preg_match('/(nocache|recache)/i',$param,$cachemode)){ 552 $cache = $cachemode[1]; 553 }else{ 554 $cache = 'cache'; 555 } 556 557 // Check whether this is a local or remote image 558 if ( preg_match('#^(https?|ftp)#i',$src) ) { 559 $call = 'externalmedia'; 560 } else { 561 $call = 'internalmedia'; 562 } 563 564 $params = array( 565 'type'=>$call, 566 'src'=>$src, 567 'title'=>$link[1], 568 'align'=>$align, 569 'width'=>$w, 570 'height'=>$h, 571 'cache'=>$cache, 572 ); 573 574 return $params; 575} 576 577//------------------------------------------------------------------------ 578class Doku_Handler_CallWriter { 579 580 var $Handler; 581 582 function Doku_Handler_CallWriter(& $Handler) { 583 $this->Handler = & $Handler; 584 } 585 586 function writeCall($call) { 587 $this->Handler->calls[] = $call; 588 } 589 590 function writeCalls($calls) { 591 $this->Handler->calls = array_merge($this->Handler->calls, $calls); 592 } 593} 594 595//------------------------------------------------------------------------ 596class Doku_Handler_List { 597 598 var $CallWriter; 599 600 var $calls = array(); 601 var $listCalls = array(); 602 var $listStack = array(); 603 604 function Doku_Handler_List(& $CallWriter) { 605 $this->CallWriter = & $CallWriter; 606 } 607 608 function writeCall($call) { 609 $this->calls[] = $call; 610 } 611 612 // Probably not needed but just in case... 613 function writeCalls($calls) { 614 $this->calls = array_merge($this->calls, $calls); 615 $this->CallWriter->writeCalls($this->calls); 616 } 617 618 //------------------------------------------------------------------------ 619 function process() { 620 foreach ( $this->calls as $call ) { 621 switch ($call[0]) { 622 case 'list_item': 623 $this->listOpen($call); 624 break; 625 case 'list_open': 626 $this->listStart($call); 627 break; 628 case 'list_close': 629 $this->listEnd($call); 630 break; 631 default: 632 $this->listContent($call); 633 break; 634 } 635 } 636 637 $this->CallWriter->writeCalls($this->listCalls); 638 } 639 640 //------------------------------------------------------------------------ 641 function listStart($call) { 642 $depth = $this->interpretSyntax($call[1][0], $listType); 643 644 $this->initialDepth = $depth; 645 $this->listStack[] = array($listType, $depth); 646 647 $this->listCalls[] = array('list'.$listType.'_open',array(),$call[2]); 648 $this->listCalls[] = array('listitem_open',array(1),$call[2]); 649 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 650 } 651 652 //------------------------------------------------------------------------ 653 function listEnd($call) { 654 $closeContent = TRUE; 655 656 while ( $list = array_pop($this->listStack) ) { 657 if ( $closeContent ) { 658 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 659 $closeContent = FALSE; 660 } 661 $this->listCalls[] = array('listitem_close',array(),$call[2]); 662 $this->listCalls[] = array('list'.$list[0].'_close', array(), $call[2]); 663 } 664 } 665 666 //------------------------------------------------------------------------ 667 function listOpen($call) { 668 $depth = $this->interpretSyntax($call[1][0], $listType); 669 $end = end($this->listStack); 670 671 // Not allowed to be shallower than initialDepth 672 if ( $depth < $this->initialDepth ) { 673 $depth = $this->initialDepth; 674 } 675 676 //------------------------------------------------------------------------ 677 if ( $depth == $end[1] ) { 678 679 // Just another item in the list... 680 if ( $listType == $end[0] ) { 681 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 682 $this->listCalls[] = array('listitem_close',array(),$call[2]); 683 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 684 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 685 686 // Switched list type... 687 } else { 688 689 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 690 $this->listCalls[] = array('listitem_close',array(),$call[2]); 691 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 692 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 693 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 694 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 695 696 array_pop($this->listStack); 697 $this->listStack[] = array($listType, $depth); 698 } 699 700 //------------------------------------------------------------------------ 701 // Getting deeper... 702 } else if ( $depth > $end[1] ) { 703 704 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 705 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 706 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 707 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 708 709 $this->listStack[] = array($listType, $depth); 710 711 //------------------------------------------------------------------------ 712 // Getting shallower ( $depth < $end[1] ) 713 } else { 714 $this->listCalls[] = array('listcontent_close',array(),$call[2]); 715 $this->listCalls[] = array('listitem_close',array(),$call[2]); 716 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 717 718 // Throw away the end - done 719 array_pop($this->listStack); 720 721 while (1) { 722 $end = end($this->listStack); 723 724 if ( $end[1] <= $depth ) { 725 726 // Normalize depths 727 $depth = $end[1]; 728 729 $this->listCalls[] = array('listitem_close',array(),$call[2]); 730 731 if ( $end[0] == $listType ) { 732 $this->listCalls[] = array('listitem_open',array($depth-1),$call[2]); 733 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 734 735 } else { 736 // Switching list type... 737 $this->listCalls[] = array('list'.$end[0].'_close', array(), $call[2]); 738 $this->listCalls[] = array('list'.$listType.'_open', array(), $call[2]); 739 $this->listCalls[] = array('listitem_open', array($depth-1), $call[2]); 740 $this->listCalls[] = array('listcontent_open',array(),$call[2]); 741 742 array_pop($this->listStack); 743 $this->listStack[] = array($listType, $depth); 744 } 745 746 break; 747 748 // Haven't dropped down far enough yet.... ( $end[1] > $depth ) 749 } else { 750 751 $this->listCalls[] = array('listitem_close',array(),$call[2]); 752 $this->listCalls[] = array('list'.$end[0].'_close',array(),$call[2]); 753 754 array_pop($this->listStack); 755 756 } 757 758 } 759 760 } 761 } 762 763 //------------------------------------------------------------------------ 764 function listContent($call) { 765 $this->listCalls[] = $call; 766 } 767 768 //------------------------------------------------------------------------ 769 function interpretSyntax($match, & $type) { 770 if ( substr($match,-1) == '*' ) { 771 $type = 'u'; 772 } else { 773 $type = 'o'; 774 } 775 return count(explode(' ',str_replace("\t",' ',$match))); 776 } 777} 778 779//------------------------------------------------------------------------ 780class Doku_Handler_Preformatted { 781 782 var $CallWriter; 783 784 var $calls = array(); 785 var $pos; 786 var $text =''; 787 788 789 790 function Doku_Handler_Preformatted(& $CallWriter) { 791 $this->CallWriter = & $CallWriter; 792 } 793 794 function writeCall($call) { 795 $this->calls[] = $call; 796 } 797 798 // Probably not needed but just in case... 799 function writeCalls($calls) { 800 $this->calls = array_merge($this->calls, $calls); 801 $this->CallWriter->writeCalls($this->calls); 802 } 803 804 function process() { 805 foreach ( $this->calls as $call ) { 806 switch ($call[0]) { 807 case 'preformatted_start': 808 $this->pos = $call[2]; 809 break; 810 case 'preformatted_newline': 811 $this->text .= "\n"; 812 break; 813 case 'preformatted_content': 814 $this->text .= $call[1][0]; 815 break; 816 case 'preformatted_end': 817 $this->CallWriter->writeCall(array('preformatted',array($this->text),$this->pos)); 818 break; 819 } 820 } 821 } 822} 823 824//------------------------------------------------------------------------ 825class Doku_Handler_Quote { 826 827 var $CallWriter; 828 829 var $calls = array(); 830 831 var $quoteCalls = array(); 832 833 function Doku_Handler_Quote(& $CallWriter) { 834 $this->CallWriter = & $CallWriter; 835 } 836 837 function writeCall($call) { 838 $this->calls[] = $call; 839 } 840 841 // Probably not needed but just in case... 842 function writeCalls($calls) { 843 $this->calls = array_merge($this->calls, $calls); 844 $this->CallWriter->writeCalls($this->calls); 845 } 846 847 function process() { 848 849 $quoteDepth = 1; 850 851 foreach ( $this->calls as $call ) { 852 switch ($call[0]) { 853 854 case 'quote_start': 855 856 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 857 858 case 'quote_newline': 859 860 $quoteLength = $this->getDepth($call[1][0]); 861 862 if ( $quoteLength > $quoteDepth ) { 863 $quoteDiff = $quoteLength - $quoteDepth; 864 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 865 $this->quoteCalls[] = array('quote_open',array(),$call[2]); 866 } 867 } else if ( $quoteLength < $quoteDepth ) { 868 $quoteDiff = $quoteDepth - $quoteLength; 869 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 870 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 871 } 872 } 873 874 $quoteDepth = $quoteLength; 875 876 break; 877 878 case 'quote_end': 879 880 if ( $quoteDepth > 1 ) { 881 $quoteDiff = $quoteDepth - 1; 882 for ( $i = 1; $i <= $quoteDiff; $i++ ) { 883 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 884 } 885 } 886 887 $this->quoteCalls[] = array('quote_close',array(),$call[2]); 888 889 $this->CallWriter->writeCalls($this->quoteCalls); 890 break; 891 892 default: 893 $this->quoteCalls[] = $call; 894 break; 895 } 896 } 897 } 898 899 function getDepth($marker) { 900 preg_match('/>{1,}/', $marker, $matches); 901 $quoteLength = strlen($matches[0]); 902 return $quoteLength; 903 } 904} 905 906//------------------------------------------------------------------------ 907class Doku_Handler_Table { 908 909 var $CallWriter; 910 911 var $calls = array(); 912 var $tableCalls = array(); 913 var $maxCols = 0; 914 var $maxRows = 1; 915 var $currentCols = 0; 916 var $firstCell = FALSE; 917 var $lastCellType = 'tablecell'; 918 919 function Doku_Handler_Table(& $CallWriter) { 920 $this->CallWriter = & $CallWriter; 921 } 922 923 function writeCall($call) { 924 $this->calls[] = $call; 925 } 926 927 // Probably not needed but just in case... 928 function writeCalls($calls) { 929 $this->calls = array_merge($this->calls, $calls); 930 $this->CallWriter->writeCalls($this->calls); 931 } 932 933 //------------------------------------------------------------------------ 934 function process() { 935 foreach ( $this->calls as $call ) { 936 switch ( $call[0] ) { 937 case 'table_start': 938 $this->tableStart($call); 939 break; 940 case 'table_row': 941 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 942 $this->tableRowOpen(array('tablerow_open',$call[1],$call[2])); 943 break; 944 case 'tableheader': 945 case 'tablecell': 946 $this->tableCell($call); 947 break; 948 case 'table_end': 949 $this->tableRowClose(array('tablerow_close',$call[1],$call[2])); 950 $this->tableEnd($call); 951 break; 952 default: 953 $this->tableDefault($call); 954 break; 955 } 956 } 957 $this->CallWriter->writeCalls($this->tableCalls); 958 } 959 960 function tableStart($call) { 961 $this->tableCalls[] = array('table_open',array(),$call[2]); 962 $this->tableCalls[] = array('tablerow_open',array(),$call[2]); 963 $this->firstCell = TRUE; 964 } 965 966 function tableEnd($call) { 967 $this->tableCalls[] = array('table_close',array(),$call[2]); 968 $this->finalizeTable(); 969 } 970 971 function tableRowOpen($call) { 972 $this->tableCalls[] = $call; 973 $this->currentCols = 0; 974 $this->firstCell = TRUE; 975 $this->lastCellType = 'tablecell'; 976 $this->maxRows++; 977 } 978 979 function tableRowClose($call) { 980 // Strip off final cell opening and anything after it 981 while ( $discard = array_pop($this->tableCalls ) ) { 982 983 if ( $discard[0] == 'tablecell_open' || $discard[0] == 'tableheader_open') { 984 985 // Its a spanning element - put it back and close it 986 if ( $discard[1][0] > 1 ) { 987 988 $this->tableCalls[] = $discard; 989 if ( strstr($discard[0],'cell') ) { 990 $name = 'tablecell'; 991 } else { 992 $name = 'tableheader'; 993 } 994 $this->tableCalls[] = array($name.'_close',array(),$call[2]); 995 } 996 997 break; 998 } 999 } 1000 $this->tableCalls[] = $call; 1001 1002 if ( $this->currentCols > $this->maxCols ) { 1003 $this->maxCols = $this->currentCols; 1004 } 1005 } 1006 1007 function tableCell($call) { 1008 if ( !$this->firstCell ) { 1009 1010 // Increase the span 1011 $lastCall = end($this->tableCalls); 1012 1013 // A cell call which follows an open cell means an empty cell so span 1014 if ( $lastCall[0] == 'tablecell_open' || $lastCall[0] == 'tableheader_open' ) { 1015 $this->tableCalls[] = array('colspan',array(),$call[2]); 1016 1017 } 1018 1019 $this->tableCalls[] = array($this->lastCellType.'_close',array(),$call[2]); 1020 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1021 $this->lastCellType = $call[0]; 1022 1023 } else { 1024 1025 $this->tableCalls[] = array($call[0].'_open',array(1,NULL),$call[2]); 1026 $this->lastCellType = $call[0]; 1027 $this->firstCell = FALSE; 1028 1029 } 1030 1031 $this->currentCols++; 1032 } 1033 1034 function tableDefault($call) { 1035 $this->tableCalls[] = $call; 1036 } 1037 1038 function finalizeTable() { 1039 1040 // Add the max cols and rows to the table opening 1041 if ( $this->tableCalls[0][0] == 'table_open' ) { 1042 // Adjust to num cols not num col delimeters 1043 $this->tableCalls[0][1][] = $this->maxCols - 1; 1044 $this->tableCalls[0][1][] = $this->maxRows; 1045 } else { 1046 trigger_error('First element in table call list is not table_open'); 1047 } 1048 1049 $lastRow = 0; 1050 $lastCell = 0; 1051 $toDelete = array(); 1052 1053 // Look for the colspan elements and increment the colspan on the 1054 // previous non-empty opening cell. Once done, delete all the cells 1055 // that contain colspans 1056 foreach ( $this->tableCalls as $key => $call ) { 1057 1058 if ( $call[0] == 'tablerow_open' ) { 1059 1060 $lastRow = $key; 1061 1062 } else if ( $call[0] == 'tablecell_open' || $call[0] == 'tableheader_open' ) { 1063 1064 $lastCell = $key; 1065 1066 } else if ( $call[0] == 'table_align' ) { 1067 1068 // If the previous element was a cell open, align right 1069 if ( $this->tableCalls[$key-1][0] == 'tablecell_open' || $this->tableCalls[$key-1][0] == 'tableheader_open' ) { 1070 $this->tableCalls[$key-1][1][1] = 'right'; 1071 1072 // If the next element if the close of an element, align either center or left 1073 } else if ( $this->tableCalls[$key+1][0] == 'tablecell_close' || $this->tableCalls[$key+1][0] == 'tableheader_close' ) { 1074 if ( $this->tableCalls[$lastCell][1][1] == 'right' ) { 1075 $this->tableCalls[$lastCell][1][1] = 'center'; 1076 } else { 1077 $this->tableCalls[$lastCell][1][1] = 'left'; 1078 } 1079 1080 } 1081 1082 // Now convert the whitespace back to cdata 1083 $this->tableCalls[$key][0] = 'cdata'; 1084 1085 } else if ( $call[0] == 'colspan' ) { 1086 1087 $this->tableCalls[$key-1][1][0] = FALSE; 1088 1089 for($i = $key-2; $i > $lastRow; $i--) { 1090 1091 if ( $this->tableCalls[$i][0] == 'tablecell_open' || $this->tableCalls[$i][0] == 'tableheader_open' ) { 1092 1093 if ( FALSE !== $this->tableCalls[$i][1][0] ) { 1094 $this->tableCalls[$i][1][0]++; 1095 break; 1096 } 1097 1098 1099 } 1100 } 1101 1102 $toDelete[] = $key-1; 1103 $toDelete[] = $key; 1104 $toDelete[] = $key+1; 1105 } 1106 } 1107 1108 foreach ( $toDelete as $delete ) { 1109 unset($this->tableCalls[$delete]); 1110 } 1111 1112 $this->tableCalls = array_values($this->tableCalls); 1113 } 1114} 1115 1116//------------------------------------------------------------------------ 1117class Doku_Handler_Section { 1118 1119 function process($calls) { 1120 1121 $sectionCalls = array(); 1122 $inSection = FALSE; 1123 1124 foreach ( $calls as $call ) { 1125 1126 if ( $call[0] == 'header' ) { 1127 1128 if ( $inSection ) { 1129 $sectionCalls[] = array('section_close',array(), $call[2]); 1130 } 1131 1132 $sectionCalls[] = $call; 1133 $sectionCalls[] = array('section_open',array($call[1][1]), $call[2]); 1134 $inSection = TRUE; 1135 1136 } else { 1137 $sectionCalls[] = $call; 1138 } 1139 } 1140 1141 if ( $inSection ) { 1142 $sectionCalls[] = array('section_close',array(), $call[2]); 1143 } 1144 1145 return $sectionCalls; 1146 } 1147 1148} 1149 1150//------------------------------------------------------------------------ 1151class Doku_Handler_Block { 1152 1153 var $calls = array(); 1154 1155 var $blockStack = array(); 1156 1157 var $inParagraph = FALSE; 1158 var $atStart = TRUE; 1159 var $skipEolKey = -1; 1160 1161 // Blocks don't contain linefeeds 1162 var $blockOpen = array( 1163 'header', 1164 'listu_open','listo_open','listitem_open', 1165 'table_open','tablerow_open','tablecell_open','tableheader_open', 1166 'quote_open', 1167 'section_open', // Needed to prevent p_open between header and section_open 1168 'code','file','php','html','hr','preformatted', 1169 ); 1170 1171 var $blockClose = array( 1172 'header', 1173 'listu_close','listo_close','listitem_close', 1174 'table_close','tablerow_close','tablecell_close','tableheader_close', 1175 'quote_close', 1176 'section_close', // Needed to prevent p_close after section_close 1177 'code','file','php','html','hr','preformatted', 1178 ); 1179 1180 // Stacks can contain linefeeds 1181 var $stackOpen = array( 1182 'footnote_open','section_open', 1183 ); 1184 1185 var $stackClose = array( 1186 'footnote_close','section_close', 1187 ); 1188 1189 function closeParagraph($pos){ 1190 // look back if there was any content - we don't want empty paragraphs 1191 $content = ''; 1192 for($i=count($this->calls)-1; $i>=0; $i--){ 1193 if($this->calls[$i][0] == 'p_open'){ 1194 break; 1195 }elseif($this->calls[$i][0] == 'cdata'){ 1196 $content .= $this->calls[$i][1][0]; 1197 }else{ 1198 $content = 'found markup'; 1199 break; 1200 } 1201 } 1202 1203 if(trim($content)==''){ 1204 //remove the whole paragraph 1205 array_splice($this->calls,$i); 1206 }else{ 1207 $this->calls[] = array('p_close',array(), $pos); 1208 } 1209 } 1210 1211 function process($calls) { 1212 foreach ( $calls as $key => $call ) { 1213 1214 // Process blocks which are stack like... (contain linefeeds) 1215 if ( in_array($call[0],$this->stackOpen ) ) { 1216 /* 1217 if ( $this->atStart ) { 1218 $this->calls[] = array('p_open',array(), $call[2]); 1219 $this->atStart = FALSE; 1220 $this->inParagraph = TRUE; 1221 } 1222 */ 1223 $this->calls[] = $call; 1224 1225 // Hack - footnotes shouldn't immediately contain a p_open 1226 if ( $call[0] != 'footnote_open' ) { 1227 $this->addToStack(); 1228 } else { 1229 $this->addToStack(FALSE); 1230 } 1231 continue; 1232 } 1233 1234 if ( in_array($call[0],$this->stackClose ) ) { 1235 1236 if ( $this->inParagraph ) { 1237 //$this->calls[] = array('p_close',array(), $call[2]); 1238 $this->closeParagraph($call[2]); 1239 } 1240 $this->calls[] = $call; 1241 $this->removeFromStack(); 1242 continue; 1243 } 1244 1245 if ( !$this->atStart ) { 1246 1247 if ( $call[0] == 'eol' ) { 1248 1249 1250 /* XXX 1251 if ( $this->inParagraph ) { 1252 $this->calls[] = array('p_close',array(), $call[2]); 1253 } 1254 $this->calls[] = array('p_open',array(), $call[2]); 1255 $this->inParagraph = TRUE; 1256 */ 1257 1258 # Check this isn't an eol instruction to skip... 1259 if ( $this->skipEolKey != $key ) { 1260 # Look to see if the next instruction is an EOL 1261 if ( isset($calls[$key+1]) && $calls[$key+1][0] == 'eol' ) { 1262 1263 if ( $this->inParagraph ) { 1264 //$this->calls[] = array('p_close',array(), $call[2]); 1265 $this->closeParagraph($call[2]); 1266 } 1267 1268 $this->calls[] = array('p_open',array(), $call[2]); 1269 $this->inParagraph = TRUE; 1270 1271 1272 # Mark the next instruction for skipping 1273 $this->skipEolKey = $key+1; 1274 1275 }else{ 1276 //if this is just a single eol make a space from it 1277 $this->calls[] = array('cdata',array(" "), $call[2]); 1278 } 1279 } 1280 1281 1282 } else { 1283 1284 $storeCall = TRUE; 1285 1286 if ( $this->inParagraph && in_array($call[0], $this->blockOpen) ) { 1287 //$this->calls[] = array('p_close',array(), $call[2]); 1288 $this->closeParagraph($call[2]); 1289 $this->inParagraph = FALSE; 1290 $this->calls[] = $call; 1291 $storeCall = FALSE; 1292 } 1293 1294 if ( in_array($call[0], $this->blockClose) ) { 1295 if ( $this->inParagraph ) { 1296 //$this->calls[] = array('p_close',array(), $call[2]); 1297 $this->closeParagraph($call[2]); 1298 $this->inParagraph = FALSE; 1299 } 1300 if ( $storeCall ) { 1301 $this->calls[] = $call; 1302 $storeCall = FALSE; 1303 } 1304 1305 // This really sucks and suggests this whole class sucks but... 1306 if ( isset($calls[$key+1]) 1307 && 1308 !in_array($calls[$key+1][0], $this->blockOpen) 1309 && 1310 !in_array($calls[$key+1][0], $this->blockClose) 1311 ) { 1312 1313 $this->calls[] = array('p_open',array(), $call[2]); 1314 $this->inParagraph = TRUE; 1315 } 1316 } 1317 1318 if ( $storeCall ) { 1319 $this->calls[] = $call; 1320 } 1321 1322 } 1323 1324 1325 } else { 1326 1327 // Unless there's already a block at the start, start a paragraph 1328 if ( !in_array($call[0],$this->blockOpen) ) { 1329 $this->calls[] = array('p_open',array(), $call[2]); 1330 if ( $call[0] != 'eol' ) { 1331 $this->calls[] = $call; 1332 } 1333 $this->atStart = FALSE; 1334 $this->inParagraph = TRUE; 1335 } else { 1336 $this->calls[] = $call; 1337 $this->atStart = FALSE; 1338 } 1339 1340 } 1341 1342 } 1343 1344 if ( $this->inParagraph ) { 1345 if ( $call[0] == 'p_open' ) { 1346 // Ditch the last call 1347 array_pop($this->calls); 1348 } else if ( !in_array($call[0], $this->blockClose) ) { 1349 //$this->calls[] = array('p_close',array(), $call[2]); 1350 $this->closeParagraph($call[2]); 1351 } else { 1352 $last_call = array_pop($this->calls); 1353 //$this->calls[] = array('p_close',array(), $call[2]); 1354 $this->closeParagraph($call[2]); 1355 $this->calls[] = $last_call; 1356 } 1357 } 1358 1359 return $this->calls; 1360 } 1361 1362 function addToStack($newStart = TRUE) { 1363 $this->blockStack[] = array($this->atStart, $this->inParagraph); 1364 $this->atStart = $newStart; 1365 $this->inParagraph = FALSE; 1366 } 1367 1368 function removeFromStack() { 1369 $state = array_pop($this->blockStack); 1370 $this->atStart = $state[0]; 1371 $this->inParagraph = $state[1]; 1372 } 1373} 1374//------------------------------------------------------------------------ 1375define('DOKU_TOC_OPEN',1); 1376define('DOKU_TOCBRANCH_OPEN',2); 1377define('DOKU_TOCITEM_OPEN',3); 1378define('DOKU_TOC_ELEMENT',4); 1379define('DOKU_TOCITEM_CLOSE',5); 1380define('DOKU_TOCBRANCH_CLOSE',6); 1381define('DOKU_TOC_CLOSE',7); 1382 1383class Doku_Handler_Toc { 1384 1385 var $calls = array(); 1386 var $tocStack = array(); 1387 var $toc = array(); 1388 var $numHeaders = 0; 1389 1390 function process($calls) { 1391 #FIXME can this be done better? 1392 global $conf; 1393 1394 foreach ( $calls as $call ) { 1395 if ( $call[0] == 'header' && $call[1][1] <= $conf['maxtoclevel'] ) { 1396 $this->numHeaders++; 1397 $this->addToToc($call); 1398 } 1399 $this->calls[] = $call; 1400 } 1401 1402 // Complete the table of contents then prepend to the calls 1403 $this->finalizeToc($call); 1404 return $this->calls; 1405 } 1406 1407 function addToToc($call) { 1408 1409 $depth = $call[1][1]; 1410 1411 // If it's the opening item... 1412 if ( count ( $this->toc) == 0 ) { 1413 1414 $this->addTocCall($call, DOKU_TOC_OPEN); 1415 1416 for ( $i = 1; $i <= $depth; $i++ ) { 1417 1418 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1419 1420 if ( $i != $depth ) { 1421 $this->addTocCall(array($call[0],array($call[1][0], $i, TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1422 } else { 1423 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1424 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOC_ELEMENT); 1425 } 1426 1427 $this->tocStack[] = $i; 1428 1429 } 1430 return; 1431 } 1432 1433 $currentDepth = end($this->tocStack); 1434 $initialDepth = $currentDepth; 1435 1436 // Create new branches as needed 1437 if ( $depth > $currentDepth ) { 1438 1439 for ($i = $currentDepth+1; $i <= $depth; $i++ ) { 1440 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_OPEN); 1441 // It's just a filler 1442 if ( $i != $depth ) { 1443 $this->addTocCall(array($call[0],array($call[1][0], $i, TRUE),$call[2]), DOKU_TOCITEM_OPEN); 1444 } else { 1445 $this->addTocCall(array($call[0],array($call[1][0], $i),$call[2]), DOKU_TOCITEM_OPEN); 1446 } 1447 $this->tocStack[] = $i; 1448 } 1449 1450 $currentDepth = $i-1; 1451 1452 } 1453 1454 // Going down 1455 if ( $depth < $currentDepth ) { 1456 for ( $i = $currentDepth; $i >= $depth; $i-- ) { 1457 if ( $i != $depth ) { 1458 array_pop($this->tocStack); 1459 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1460 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCBRANCH_CLOSE); 1461 } else { 1462 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_CLOSE); 1463 $this->addTocCall(array($call[0],array($call[1][0],$i),$call[2]), DOKU_TOCITEM_OPEN); 1464 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1465 return; 1466 } 1467 } 1468 } 1469 1470 if ( $depth == $initialDepth ) { 1471 $this->addTocCall($call, DOKU_TOCITEM_CLOSE); 1472 $this->addTocCall($call, DOKU_TOCITEM_OPEN); 1473 } 1474 1475 $this->addTocCall($call, DOKU_TOC_ELEMENT); 1476 1477 1478 } 1479 1480 function addTocCall($call, $type) { 1481 switch ( $type ) { 1482 case DOKU_TOC_OPEN: 1483 $this->toc[] = array('toc_open',array(),$call[2]); 1484 break; 1485 1486 case DOKU_TOCBRANCH_OPEN: 1487 $this->toc[] = array('tocbranch_open',array($call[1][1]),$call[2]); 1488 break; 1489 1490 case DOKU_TOCITEM_OPEN: 1491 if ( isset( $call[1][2] ) ) { 1492 $this->toc[] = array('tocitem_open',array($call[1][1], TRUE),$call[2]); 1493 } else { 1494 $this->toc[] = array('tocitem_open',array($call[1][1]),$call[2]); 1495 } 1496 break; 1497 1498 case DOKU_TOC_ELEMENT: 1499 $this->toc[] = array('tocelement',array($call[1][1],$call[1][0]),$call[2]); 1500 break; 1501 1502 case DOKU_TOCITEM_CLOSE: 1503 $this->toc[] = array('tocitem_close',array($call[1][1]),$call[2]); 1504 break; 1505 1506 case DOKU_TOCBRANCH_CLOSE: 1507 $this->toc[] = array('tocbranch_close',array($call[1][1]),$call[2]); 1508 break; 1509 1510 case DOKU_TOC_CLOSE: 1511 if ( count($this->toc) > 0 ) { 1512 $this->toc[] = array('toc_close',array(),$call[2]); 1513 } 1514 break; 1515 } 1516 } 1517 1518 function finalizeToc($call) { 1519 if ( $this->numHeaders < 3 ) { 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