1<?php 2 3/* 4 * This plugin extends DokuWiki's list markup syntax to allow definition lists 5 * and list items with multiple paragraphs. The complete syntax is as follows: 6 * 7 * 8 * - ordered list item [<ol><li>] <!-- as standard syntax --> 9 * * unordered list item [<ul><li>] <!-- as standard syntax --> 10 * ? definition list term [<dl><dt>] 11 * : definition list definition [<dl><dd>] 12 * 13 * -- ordered list item w/ multiple paragraphs 14 * ** unordered list item w/ multiple paragraphs 15 * :: definition list definition w/multiple paragraphs 16 * .. new paragraph in --, **, or :: 17 * 18 * 19 * Lists can be nested within lists, just as in the standard DokuWiki syntax. 20 * 21 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 22 * @author Ben Slusky <sluskyb@paranoiacs.org> 23 * 24 */ 25 26class syntax_plugin_yalist extends DokuWiki_Syntax_Plugin { 27 private static $odt_table_stack = array(); 28 private static $odt_table_stack_index = 0; 29 private $stack = array(); 30 31 public function getType() { 32 return 'container'; 33 } 34 35 public function getSort() { 36 // just before listblock (10) 37 return 9; 38 } 39 40 public function getPType() { 41 return 'block'; 42 } 43 44 public function getAllowedTypes() { 45 return array('substition', 'protected', 'disabled', 'formatting'); 46 } 47 48 public function connectTo($mode) { 49 $this->Lexer->addEntryPattern('\n {2,}(?:--?|\*\*?|\?|::?)', $mode, 'plugin_yalist'); 50 $this->Lexer->addEntryPattern('\n\t{1,}(?:--?|\*\*?|\?|::?)', $mode, 'plugin_yalist'); 51 $this->Lexer->addPattern('\n {2,}(?:--?|\*\*?|\?|::?|\.\.)', 'plugin_yalist'); 52 $this->Lexer->addPattern('\n\t{1,}(?:--?|\*\*?|\?|::?|\.\.)', 'plugin_yalist'); 53 } 54 55 public function postConnect() { 56 $this->Lexer->addExitPattern('\n', 'plugin_yalist'); 57 } 58 59 public function handle($match, $state, $pos, Doku_Handler $handler) { 60 $output = array(); 61 $level = 0; 62 switch($state) { 63 case DOKU_LEXER_ENTER: 64 $frame = $this->interpretMatch($match); 65 $level = $frame['level'] = 1; 66 array_push( 67 $output, 68 "${frame['list']}_open", 69 "${frame['item']}_open", 70 "${frame['item']}_content_open" 71 ); 72 if($frame['paras']) { 73 array_push($output, 'p_open'); 74 } 75 array_push($this->stack, $frame); 76 break; 77 case DOKU_LEXER_EXIT: 78 $close_content = true; 79 while($frame = array_pop($this->stack)) { 80 // for the first frame we pop off the stack, we'll need to 81 // close the content tag; for the rest it will have been 82 // closed already 83 if($close_content) { 84 if($frame['paras']) { 85 array_push($output, 'p_close'); 86 } 87 array_push($output, "${frame['item']}_content_close"); 88 $close_content = false; 89 } 90 array_push( 91 $output, 92 "${frame['item']}_close", 93 "${frame['list']}_close" 94 ); 95 } 96 break; 97 case DOKU_LEXER_MATCHED: 98 $last_frame = end($this->stack); 99 if(substr($match, -2) == '..') { 100 // new paragraphs cannot be deeper than the current depth, 101 // but they may be shallower 102 $para_depth = count(explode(' ', str_replace("\t", ' ', $match))); 103 $close_content = true; 104 while($para_depth < $last_frame['depth'] && count($this->stack) > 1) { 105 if($close_content) { 106 if($last_frame['paras']) { 107 array_push($output, 'p_close'); 108 } 109 array_push($output, "${last_frame['item']}_content_close"); 110 $close_content = false; 111 } 112 array_push( 113 $output, 114 "${last_frame['item']}_close", 115 "${last_frame['list']}_close" 116 ); 117 array_pop($this->stack); 118 $last_frame = end($this->stack); 119 } 120 if($last_frame['paras']) { 121 if($close_content) { 122 // depth did not change 123 array_push($output, 'p_close', 'p_open'); 124 } else { 125 array_push( 126 $output, 127 "${last_frame['item']}_content_open", 128 'p_open' 129 ); 130 } 131 } else { 132 // let's just pretend we didn't match... 133 $state = DOKU_LEXER_UNMATCHED; 134 $output = $match; 135 } 136 break; 137 } 138 $curr_frame = $this->interpretMatch($match); 139 if($curr_frame['depth'] > $last_frame['depth']) { 140 // going one level deeper 141 $level = $last_frame['level'] + 1; 142 if($last_frame['paras']) { 143 array_push($output, 'p_close'); 144 } 145 array_push( 146 $output, 147 "${last_frame['item']}_content_close", 148 "${curr_frame['list']}_open" 149 ); 150 } else { 151 // same depth, or getting shallower 152 $close_content = true; 153 // keep popping frames off the stack until we find a frame 154 // that's at least as deep as this one, or until only the 155 // bottom frame (i.e. the initial list markup) remains 156 while($curr_frame['depth'] < $last_frame['depth'] && 157 count($this->stack) > 1) { 158 // again, we need to close the content tag only for 159 // the first frame popped off the stack 160 if($close_content) { 161 if($last_frame['paras']) { 162 array_push($output, 'p_close'); 163 } 164 array_push($output, "${last_frame['item']}_content_close"); 165 $close_content = false; 166 } 167 array_push( 168 $output, 169 "${last_frame['item']}_close", 170 "${last_frame['list']}_close" 171 ); 172 array_pop($this->stack); 173 $last_frame = end($this->stack); 174 } 175 // pull the last frame off the stack; 176 // it will be replaced by the current frame 177 array_pop($this->stack); 178 $level = $last_frame['level']; 179 if($close_content) { 180 if($last_frame['paras']) { 181 array_push($output, 'p_close'); 182 } 183 array_push($output, "${last_frame['item']}_content_close"); 184 $close_content = false; 185 } 186 array_push($output, "${last_frame['item']}_close"); 187 if($curr_frame['list'] != $last_frame['list']) { 188 // change list types 189 array_push( 190 $output, 191 "${last_frame['list']}_close", 192 "${curr_frame['list']}_open" 193 ); 194 } 195 } 196 // and finally, open tags for the new list item 197 array_push( 198 $output, 199 "${curr_frame['item']}_open", 200 "${curr_frame['item']}_content_open" 201 ); 202 if($curr_frame['paras']) { 203 array_push($output, 'p_open'); 204 } 205 $curr_frame['level'] = $level; 206 array_push($this->stack, $curr_frame); 207 break; 208 case DOKU_LEXER_UNMATCHED: 209 $output = $match; 210 break; 211 } 212 return array('state' => $state, 'output' => $output, 'level' => $level); 213 } 214 215 private function interpretMatch($match) { 216 $tag_table = array( 217 '*' => 'u_li', 218 '-' => 'o_li', 219 '?' => 'dt', 220 ':' => 'dd', 221 ); 222 $tag = $tag_table[substr($match, -1)]; 223 return array( 224 'depth' => count(explode(' ', str_replace("\t", ' ', $match))), 225 'list' => substr($tag, 0, 1) . 'l', 226 'item' => substr($tag, -2), 227 'paras' => (substr($match, -1) == substr($match, -2, 1)), 228 ); 229 } 230 231 public function render($mode, Doku_Renderer $renderer, $data) { 232 if($mode != 'xhtml' && $mode != 'latex' && $mode != 'odt') { 233 return false; 234 } 235 if($data['state'] == DOKU_LEXER_UNMATCHED) { 236 if($mode != 'odt') { 237 $renderer->doc .= $renderer->_xmlEntities($data['output']); 238 } else { 239 $renderer->cdata($data['output']); 240 } 241 return true; 242 } 243 foreach($data['output'] as $i) { 244 switch($mode) { 245 case 'xhtml': 246 $this->renderXhtmlItem($renderer, $i, $data); 247 break; 248 case 'latex': 249 $this->renderLatexItem($renderer, $i, $data); 250 break; 251 case 'odt': 252 $this->renderOdtItem($renderer, $i, $data); 253 break; 254 } 255 } 256 if($data['state'] == DOKU_LEXER_EXIT) { 257 if($mode != 'odt') { 258 $renderer->doc .= "\n"; 259 } else { 260 $renderer->linebreak(); 261 } 262 } 263 return true; 264 } 265 266 private function renderXhtmlItem(Doku_Renderer $renderer, $item, $data) { 267 $markup = ''; 268 switch($item) { 269 case 'ol_open': 270 $markup = "<ol>\n"; 271 break; 272 case 'ol_close': 273 $markup = "</ol>\n"; 274 break; 275 case 'ul_open': 276 $markup = "<ul>\n"; 277 break; 278 case 'ul_close': 279 $markup = "</ul>\n"; 280 break; 281 case 'dl_open': 282 $markup = "<dl>\n"; 283 break; 284 case 'dl_close': 285 $markup = "</dl>\n"; 286 break; 287 case 'li_open': 288 $markup = "<li class=\"level${data['level']}\">"; 289 break; 290 case 'li_content_open': 291 $markup = "<div class=\"li\">\n"; 292 break; 293 case 'li_content_close': 294 case 'dd_content_close': 295 $markup = "\n</div>"; 296 break; 297 case 'li_close': 298 $markup = "</li>\n"; 299 break; 300 case 'dt_open': 301 $markup = "<dt class=\"level${data['level']}\">"; 302 break; 303 case 'dt_content_open': 304 $markup = "<span class=\"dt\">"; 305 break; 306 case 'dt_content_close': 307 $markup = "</span>"; 308 break; 309 case 'dt_close': 310 $markup = "</dt>\n"; 311 break; 312 case 'dd_open': 313 $markup = "<dd class=\"level${data['level']}\">"; 314 break; 315 case 'dd_content_open': 316 $markup = "<div class=\"dd\">\n"; 317 break; 318 case 'dd_close': 319 $markup = "</dd>\n"; 320 break; 321 case 'p_open': 322 $markup = "<p>\n"; 323 break; 324 case 'p_close': 325 $markup = "\n</p>"; 326 break; 327 } 328 $renderer->doc .= $markup; 329 } 330 331 private function renderLatexItem(Doku_Renderer $renderer, $item) { 332 $markup = ''; 333 switch($item) { 334 case 'ol_open': 335 $markup = "\\begin{enumerate}\n"; 336 break; 337 case 'ol_close': 338 $markup = "\\end{enumerate}\n"; 339 break; 340 case 'ul_open': 341 $markup = "\\begin{itemize}\n"; 342 break; 343 case 'ul_close': 344 $markup = "\\end{itemize}\n"; 345 break; 346 case 'dl_open': 347 $markup = "\\begin{description}\n"; 348 break; 349 case 'dl_close': 350 $markup = "\\end{description}\n"; 351 break; 352 case 'li_open': 353 $markup = "\item "; 354 break; 355 case 'li_content_open': 356 break; 357 case 'li_content_close': 358 break; 359 case 'li_close': 360 $markup = "\n"; 361 break; 362 case 'dt_open': 363 $markup = "\item["; 364 break; 365 case 'dt_content_open': 366 break; 367 case 'dt_content_close': 368 break; 369 case 'dt_close': 370 $markup = "] "; 371 break; 372 case 'dd_open': 373 break; 374 case 'dd_content_open': 375 break; 376 case 'dd_content_close': 377 break; 378 case 'dd_close': 379 $markup = "\n"; 380 break; 381 case 'p_open': 382 $markup = "\n"; 383 break; 384 case 'p_close': 385 $markup = "\n"; 386 break; 387 } 388 $renderer->doc .= $markup; 389 } 390 391 /** 392 * Render yalist items for ODT format 393 * 394 * @param Doku_Renderer $renderer The current renderer object 395 * @param string $item The item to render 396 * 397 * @author LarsDW223 398 */ 399 private function renderOdtItem(Doku_Renderer $renderer, $item) { 400 switch($item) { 401 case 'ol_open': 402 $renderer->listo_open(); 403 break; 404 case 'ul_open': 405 $renderer->listu_open(); 406 break; 407 case 'dl_open': 408 if($this->getConf('def_list_odt_export') != 'table') { 409 $renderer->listu_open(); 410 } else { 411 $renderer->table_open(2); 412 } 413 self::$odt_table_stack [self::$odt_table_stack_index] = array(); 414 self::$odt_table_stack [self::$odt_table_stack_index]['itemOpen'] = false; 415 self::$odt_table_stack [self::$odt_table_stack_index]['dtState'] = 0; 416 self::$odt_table_stack [self::$odt_table_stack_index]['ddState'] = 0; 417 self::$odt_table_stack_index++; 418 break; 419 case 'ol_close': 420 case 'ul_close': 421 $renderer->list_close(); 422 break; 423 case 'dl_close': 424 $config = $this->getConf('def_list_odt_export'); 425 if($config != 'table') { 426 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] != 2) { 427 if($config == 'listheader' && method_exists($renderer, 'listheader_close')) { 428 $renderer->listheader_close(); 429 } else { 430 $renderer->listitem_close(); 431 } 432 } 433 self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 0; 434 $renderer->list_close(); 435 } else { 436 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] == 0) { 437 $properties = array(); 438 $properties ['border'] = 'none'; 439 $renderer->_odtTableCellOpenUseProperties($properties); 440 $renderer->tablecell_close(); 441 } 442 self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 0; 443 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) { 444 $renderer->tablerow_close(1); 445 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false; 446 } 447 $renderer->table_close(); 448 } 449 if(self::$odt_table_stack_index > 0) { 450 self::$odt_table_stack_index--; 451 unset(self::$odt_table_stack [self::$odt_table_stack_index]); 452 } 453 break; 454 455 case 'li_open': 456 $renderer->listitem_open(1); 457 break; 458 case 'li_content_open': 459 $renderer->listcontent_open(); 460 break; 461 case 'li_content_close': 462 $renderer->listcontent_close(); 463 break; 464 case 'li_close': 465 $renderer->listitem_close(); 466 break; 467 468 case 'dt_open': // unconditional: DT tags can't contain paragraphs. That would not be legal XHTML. 469 switch($this->getConf('def_list_odt_export')) { 470 case 'listheader': 471 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) { 472 if(method_exists($renderer, 'listheader_close')) { 473 $renderer->listheader_close(); 474 } else { 475 $renderer->listitem_close(); 476 } 477 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false; 478 } 479 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) { 480 if(method_exists($renderer, 'listheader_open')) { 481 $renderer->listheader_open(1); 482 } else { 483 $renderer->listitem_open(1); 484 } 485 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true; 486 } 487 break; 488 case 'table': 489 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] == 0) { 490 $properties = array(); 491 $properties ['border'] = 'none'; 492 $renderer->_odtTableCellOpenUseProperties($properties); 493 $renderer->tablecell_close(); 494 } 495 496 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) { 497 $renderer->tablerow_close(); 498 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false; 499 } 500 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) { 501 $renderer->tablerow_open(1); 502 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true; 503 } 504 $properties = array(); 505 $properties ['border'] = 'none'; 506 $renderer->_odtTableCellOpenUseProperties($properties); 507 break; 508 default: 509 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) { 510 $renderer->listitem_close(); 511 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false; 512 } 513 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) { 514 $renderer->listitem_open(1); 515 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true; 516 } 517 break; 518 } 519 self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 1; 520 self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 0; 521 break; 522 case 'dd_open': 523 switch($this->getConf('def_list_odt_export')) { 524 case 'listheader': 525 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) { 526 if(method_exists($renderer, 'listheader_open')) { 527 $renderer->listheader_open(1); 528 } else { 529 $renderer->listitem_open(1); 530 } 531 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true; 532 } 533 break; 534 case 'table': 535 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) { 536 $renderer->tablerow_open(1); 537 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true; 538 } 539 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] == 1) { 540 $renderer->tablecell_close(); 541 } 542 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] == 0) { 543 $properties = array(); 544 $properties ['border'] = 'none'; 545 $renderer->_odtTableCellOpenUseProperties($properties); 546 $renderer->tablecell_close(); 547 } 548 549 $properties = array(); 550 $properties ['border'] = 'none'; 551 $renderer->_odtTableCellOpenUseProperties($properties); 552 break; 553 default: 554 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === false) { 555 $renderer->listitem_open(1); 556 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = true; 557 } 558 break; 559 } 560 self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 0; 561 self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 1; 562 break; 563 case 'dt_content_open': 564 switch($this->getConf('def_list_odt_export')) { 565 case 'table': 566 $renderer->p_open(); 567 break; 568 default: 569 $renderer->listcontent_open(); 570 break; 571 } 572 $this->renderODTOpenSpan($renderer); 573 break; 574 case 'dd_content_open': 575 switch($this->getConf('def_list_odt_export')) { 576 case 'table': 577 $renderer->p_open(); 578 break; 579 default: 580 $renderer->listcontent_open(); 581 break; 582 } 583 break; 584 case 'dt_content_close': 585 $this->renderODTCloseSpan($renderer); 586 switch($this->getConf('def_list_odt_export')) { 587 case 'table': 588 $renderer->p_close(); 589 break; 590 default: 591 $renderer->listcontent_close(); 592 break; 593 } 594 break; 595 case 'dd_content_close': 596 switch($this->getConf('def_list_odt_export')) { 597 case 'table': 598 $renderer->p_close(); 599 break; 600 default: 601 $renderer->listcontent_close(); 602 break; 603 } 604 break; 605 case 'dt_close': 606 switch($this->getConf('def_list_odt_export')) { 607 case 'listheader': 608 $renderer->linebreak(); 609 break; 610 case 'table': 611 $renderer->tablecell_close(); 612 self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 2; 613 break; 614 default: 615 $renderer->linebreak(); 616 break; 617 } 618 break; 619 620 case 'dd_close': 621 switch($this->getConf('def_list_odt_export')) { 622 case 'listheader': 623 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) { 624 if(method_exists($renderer, 'listheader_close')) { 625 $renderer->listheader_close(); 626 } else { 627 $renderer->listitem_close(); 628 } 629 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false; 630 } 631 break; 632 case 'table': 633 $renderer->tablecell_close(); 634 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) { 635 $renderer->tablerow_close(1); 636 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false; 637 } 638 break; 639 default: 640 if(self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] === true) { 641 $renderer->listitem_close(1); 642 self::$odt_table_stack [self::$odt_table_stack_index - 1]['itemOpen'] = false; 643 } 644 break; 645 } 646 self::$odt_table_stack [self::$odt_table_stack_index - 1]['dtState'] = 0; 647 self::$odt_table_stack [self::$odt_table_stack_index - 1]['ddState'] = 2; 648 break; 649 650 case 'p_open': 651 $renderer->p_open(); 652 break; 653 case 'p_close': 654 $renderer->p_close(); 655 break; 656 } 657 } 658 659 /** 660 * Open ODT span for rendering of dt-content 661 * 662 * @param Doku_Renderer $renderer The current renderer object 663 * 664 * @author LarsDW223 665 */ 666 private function renderODTOpenSpan($renderer) { 667 $properties = array(); 668 669 // Get CSS properties for ODT export. 670 $renderer->getODTProperties($properties, 'div', 'dokuwiki dt', null); 671 672 $renderer->_odtSpanOpenUseProperties($properties); 673 } 674 675 /** 676 * Close ODT span for rendering of dt-content 677 * 678 * @param Doku_Renderer $renderer The current renderer object 679 * 680 * @author LarsDW223 681 */ 682 private function renderODTCloseSpan($renderer) { 683 if(method_exists($renderer, '_odtSpanClose') === false) { 684 // Function is not supported by installed ODT plugin version, return. 685 return; 686 } 687 $renderer->_odtSpanClose(); 688 } 689} 690