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