1<?php 2/** 3 * DokuWiki Plugin ExtList (Syntax component) 4 * 5 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 6 * @author Satoshi Sahara <sahara.satoshi@gmail.com> 7 * 8 */ 9if (!defined('DOKU_INC')) die(); 10 11class syntax_plugin_extlist extends DokuWiki_Syntax_Plugin 12{ 13 public function getType() 14 { // Syntax Type 15 return 'container'; 16 } 17 18 public function getAllowedTypes() 19 { // Allowed Mode Types 20 return array( 21 'formatting', 22 'substition', 23 'disabled', 24 'protected', 25 ); 26 } 27 28 public function getPType() 29 { // Paragraph Type 30 return 'block'; 31 } 32 33 34 protected $stack = array(); 35 protected $list_class = array(); // store class specified by macro 36 37 // Enable hierarchical numbering for nested ordered lists 38 protected $olist_level = 0; 39 protected $olist_info = array(); 40 41 protected $use_div = true; 42 43 44 45 /** 46 * Connect pattern to lexer 47 */ 48 protected $mode; 49 protected $macro_pattern; 50 protected $entry_pattern, $match_pattern, $extra_pattern, $exit_pattern; 51 52 public function preConnect() 53 { 54 // drop 'syntax_' from class name 55 $this->mode = substr(get_class($this), 7); 56 57 // macro to specify list class 58 $this->macro_pattern = '\n(?: {2,}|\t{1,})~~(?:dl|ol|ul):[\w -]*?~~'; 59 60 // list patterns 61 $olist_pattern = '\-?\d+[.:] |-:?'; // ordered list item 62 $ulist_pattern = '\*:?'; // unordered list items 63 $dlist_pattern = ';;?|::?'; // description list item 64 $continue_pattern = '\+:?'; // continued contents to the previous item 65 66 $this->entry_pattern = '\n(?: {2,}|\t{1,})'.'(?:' 67 . $olist_pattern .'|' 68 . $ulist_pattern .'|' 69 . $dlist_pattern . ') *'; 70 $this->match_pattern = '\n(?: {2,}|\t{1,})'.'(?:' 71 . $olist_pattern .'|' 72 . $ulist_pattern .'|' 73 . $dlist_pattern .'|' 74 . $continue_pattern . ') *'; 75 76 // continued item content by indentation 77 $this->extra_pattern = '\n(?: {2,}|\t{1,})(?![-*;:?+~])'; 78 79 $this->exit_pattern = '\n'; 80 } 81 82 public function connectTo($mode) 83 { 84 $this->Lexer->addEntryPattern('[ \t]*'.$this->entry_pattern, $mode, $this->mode); 85 86 // macro syntax to specify class for next list [ul|ol|dl] 87 $this->Lexer->addSpecialPattern('[ \t]*'.$this->macro_pattern, $mode, $this->mode); 88 $this->Lexer->addPattern($this->macro_pattern, $this->mode); 89 } 90 91 public function postConnect() 92 { 93 // subsequent list item 94 $this->Lexer->addPattern($this->match_pattern, $this->mode); 95 $this->Lexer->addPattern(' ::? ', $this->mode); // dt and dd in one line 96 97 // continued list item content, indented by at least two spaces 98 $this->Lexer->addPattern($this->extra_pattern, $this->mode); 99 100 // terminate a list block 101 $this->Lexer->addExitPattern($this->exit_pattern, $this->mode); 102 } 103 104 public function getSort() 105 { // sort number used to determine priority of this mode 106 return 9; // just before listblock (10) 107 } 108 109 /** 110 * get markup and depth from the match 111 * 112 * @param $match string 113 * @return array 114 */ 115 protected function interpret($match) 116 { 117 // depth: count double spaces indent after '\n' 118 $depth = substr_count(str_replace("\t", ' ', ltrim($match,' ')),' '); 119 $match = trim($match); 120 121 $m = array('depth' => $depth); 122 123 // check order list markup with number 124 if (preg_match('/^(-?\d+)([.:])/', $match, $matches)) { 125 $m += array( 126 'mk' => ($matches[2] == '.') ? '-' : '-:', 127 'list' => 'ol', 'item' => 'li', 'num' => $matches[1] 128 ); 129 if ($matches[2] == ':') $m += array('p' => 1); 130 } else { 131 $m += array('mk' => $match); 132 133 switch (substr($match, 0, 1)) { 134 case '' : 135 $m += array('list' => NULL, 'item' => NULL); 136 break; 137 case '+': 138 $m += array('list' => NULL, 'item' => NULL); 139 if ($match == '+:') { 140 $m += array('p' => 1); 141 } else { 142 $m['mk'] = ''; 143 } 144 break; 145 case '-': // ordered list 146 $m += array('list' => 'ol', 'item' => 'li'); 147 if ($match == '-:') $m += array('p' => 1); 148 break; 149 case '*': // unordered list 150 $m += array('list' => 'ul', 'item' => 'li'); 151 if ($match == '*:') $m += array('p' => 1); 152 break; 153 case ';': // description list term 154 $m += array('list' => 'dl', 'item' => 'dt'); 155 if ($match == ';') $m += array('class' => 'compact'); 156 break; 157 case ':': // description list desc 158 $m += array('list' => 'dl', 'item' => 'dd'); 159 if ($match == '::') $m += array('p' => 1); 160 break; 161 } 162 } 163 //error_log('extlist intpret: $m='.var_export($m,1)); 164 return $m; 165 } 166 167 168 /** 169 * check whether list type has changed 170 */ 171 private function isListTypeChanged($m0, $m1) 172 { 173 return (strncmp($m0['list'], $m1['list'], 1) !== 0); 174 } 175 176 /** 177 * create marker for ordered list items 178 */ 179 private function olist_marker($level) 180 { 181 $num = $this->olist_info[$level]; 182 //error_log('olist lv='.$level.' list_class='.$this->list_class['ol'].' num='.$num); 183 184 // Parenthesized latin small letter marker: ⒜,⒝,⒞, … ,⒵ 185 if (strpos($this->list_class['ol'], 'alphabet') !== false){ 186 $modulus = ($num -1) % 26; 187 $marker = '&#'.(9372 + $modulus).';'; 188 return $marker; 189 } 190 191 // Hierarchical numbering (default): eg. 1. | 2.1 | 3.2.9 192 $marker = $this->olist_info[1]; 193 if ($level == 1) { 194 return $marker.'.'; 195 } else { 196 for ($i = 2; $i <= $level; $i++) { 197 $marker .= '.'.$this->olist_info[$i]; 198 } 199 return $marker; 200 } 201 } 202 203 /** 204 * srore class attribute for lists [ul|ol|dl] specfied by macro pattern 205 * macro_pattern = ~~(?:dl|ol|ul):[\w -]*?~~ 206 */ 207 private function storeListClass($str) 208 { 209 $str = trim($str); 210 $this->list_class[substr($str,2,2)] = trim(substr($str,5,-2)); 211 } 212 213 214 /** 215 * helper function to simplify writing plugin calls to the instruction list 216 * first three arguments are passed to function render as $data 217 * Note: this function was used in the DW exttab3 plugin. 218 */ 219 protected function _writeCall($tag, $attr, $state, $pos, $match, $handler) 220 { 221 $handler->addPluginCall($this->getPluginName(), 222 array($state, $tag, $attr), $state, $pos, $match 223 ); 224 225 } 226 227 /** 228 * write call to open a list block [ul|ol|dl] 229 */ 230 private function _openList($m, $pos, $match, $handler) 231 { 232 $tag = $m['list']; 233 // start value only for ordered list 234 if ($tag == 'ol') { 235 $attr = isset($m['num']) ? 'start="'.$m['num'].'"' : ''; 236 $this->olist_level++; // increase olist level 237 } else { 238 $attr = null; 239 } 240 // list class 241 $class = 'extlist'; 242 if (isset($this->list_class[$tag])) { 243 // Note: list_class can be empty 244 $class.= ' '.$this->list_class[$tag]; 245 } else { 246 $class.= ' '.$this->getConf($tag.'_class'); 247 } 248 $class = rtrim($class); 249 $attr.= !empty($attr) ? ' ' : ''; 250 $attr.= ' class="'.$class.'"'; 251 $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); 252 } 253 254 /** 255 * write call to close a list block [ul|ol|dl] 256 */ 257 private function _closeList($m, $pos, $match, $handler) 258 { 259 $tag = $m['list']; 260 if ($tag == 'ol') { 261 $this->olist_level--; // reduce olist level 262 } 263 $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); 264 } 265 266 /** 267 * write call to open a list item [li|dt|dd] 268 */ 269 private function _openItem($m, $pos, $match, $handler) 270 { 271 $tag = $m['item']; 272 switch ($m['mk']) { 273 case '-': 274 case '-:': 275 // prepare hierarchical marker for nested ordered list item 276 $this->olist_info[$this->olist_level] = $m['num']; 277 $lv = $this->olist_level; 278 $attr = ' value="'.$m['num'].'"'; 279 $attr.= ' data-marker="'.$this->olist_marker($lv).'"'; 280 break; 281 case ';': 282 $attr = 'class="'.$m['class'].'"'; 283 break; 284 default: 285 $attr = ''; 286 } 287 $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); 288 } 289 290 /** 291 * write call to close a list item [li|dt|dd] 292 */ 293 private function _closeItem($m, $pos, $match, $handler) 294 { 295 $tag = $m['item']; 296 $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); 297 } 298 299 /** 300 * write call to open inner wrapper [div|span] 301 */ 302 private function _openWrapper($m, $pos, $match, $handler) 303 { 304 switch ($m['mk']) { 305 case ';': // dl dt 306 case ';;': // dl dt, explicitly no-compact 307 $tag = 'span'; $attr = ''; 308 break; 309 case ':': // dl dd 310 case '::': // dl dd p 311 return; 312 break; 313 default: 314 if (!$this->use_div) return; 315 $tag = 'div'; $attr = 'class="li"'; 316 break; 317 } 318 $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); 319 } 320 321 /** 322 * write call to close inner wrapper [div|span] 323 */ 324 private function _closeWrapper($m, $pos, $match, $handler) 325 { 326 switch ($m['mk']) { 327 case ';': // dl dt 328 case ';;': // dl dt, explicitly no-compact 329 $tag = 'span'; 330 break; 331 case ':': // dl dd 332 case '::': // dl dd p 333 return; 334 break; 335 default: 336 if (!$this->use_div) return; 337 $tag = 'div'; 338 break; 339 } 340 $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); 341 } 342 343 /** 344 * write call to open paragraph (p tag) 345 */ 346 private function _openParagraph($pos, $match, $handler) 347 { 348 $this->_writeCall('p','',DOKU_LEXER_ENTER, $pos,$match,$handler); 349 } 350 351 /** 352 * write call to close paragraph (p tag) 353 */ 354 private function _closeParagraph($pos, $match, $handler) 355 { 356 $this->_writeCall('p','',DOKU_LEXER_EXIT, $pos,$match,$handler); 357 } 358 359 /** 360 * Handle the match 361 */ 362 public function handle($match, $state, $pos, Doku_Handler $handler) 363 { 364 switch ($state) { 365 case DOKU_LEXER_SPECIAL: 366 // specify class attribute for lists [ul|ol|dl] 367 $this->storeListClass($match); 368 break; 369 370 case DOKU_LEXER_ENTER: 371 $m1 = $this->interpret($match); 372 if (($m1['list'] == 'ol') && !isset($m1['num'])) { 373 $m1['num'] = 1; 374 } 375 376 // open list tag [ul|ol|dl] 377 $this->_openList($m1, $pos,$match,$handler); 378 // open item tag [li|dt|dd] 379 $this->_openItem($m1, $pos,$match,$handler); 380 // open inner wrapper [div|span] 381 $this->_openWrapper($m1, $pos,$match,$handler); 382 // open p if necessary 383 if (isset($m1['p'])) $this->_openParagraph($pos,$match,$handler); 384 385 // add to stack 386 array_push($this->stack, $m1); 387 break; 388 389 case DOKU_LEXER_UNMATCHED: 390 // cdata --- use base() as _writeCall() is prefixed for private/protected 391 $handler->base($match, $state, $pos); 392 break; 393 394 case DOKU_LEXER_EXIT: 395 // clear list_class 396 $this->list_class = array(); 397 // do not break here! 398 399 case DOKU_LEXER_MATCHED: 400 // specify class attribute for lists [ul|ol|dl] 401 if (substr($match, -2) == '~~') { 402 $this->storeListClass($match); 403 break; 404 } 405 406 // retrieve previous list item from stack 407 $m0 = array_pop($this->stack); 408 $m1 = $this->interpret($match); 409 410 // set m1 depth if dt and dd are in one line 411 if (($m1['depth'] == 0) && ($m0['item'] == 'dt')) { 412 $m1['depth'] = $m0['depth']; 413 } 414 415 // continued list item content, indented by at least two spaces 416 if (empty($m1['mk']) && ($m1['depth'] > 0)) { 417 // !!EXPERIMENTAL SCRIPTIO CONTINUA concerns!! 418 // replace indent to single space, but leave it for LineBreak2 plugin 419 $handler->base("\n", DOKU_LEXER_UNMATCHED, $pos); 420 421 // restore stack 422 array_push($this->stack, $m0); 423 break; 424 } 425 426 // close p if necessary 427 if (isset($m0['p'])) $this->_closeParagraph($pos,$match,$handler); 428 429 // close inner wrapper [div|span] if necessary 430 if ($m1['mk'] == '+:') { 431 // Paragraph markup 432 if ($m0['depth'] > $m1['depth']) { 433 $this->_closeWrapper($m0, $pos,$match,$handler); 434 } else { 435 // new paragraph can not be deeper than previous depth 436 // fix current depth quietly 437 $m1['depth'] = min($m0['depth'], $m1['depth']); 438 } 439 // fix previous p type 440 $m0['p'] = 1; 441 } else { 442 // List item markup 443 if ($m0['depth'] >= $m1['depth']) { 444 $this->_closeWrapper($m0, $pos,$match,$handler); 445 } 446 } 447 448 // List item becomes shallower - close deeper list 449 while (isset($m0['depth']) && ($m0['depth'] > $m1['depth'])) { 450 // close item [li|dt|dd] 451 $this->_closeItem($m0, $pos,$match,$handler); 452 // close list [ul|ol|dl] 453 $this->_closeList($m0, $pos,$match,$handler); 454 455 $m0 = array_pop($this->stack); 456 } 457 458 // Break out of switch structure if end of list block 459 if ($state == DOKU_LEXER_EXIT) { 460 break; 461 } 462 463 // Paragraph markup 464 if ($m1['mk'] == '+:') { 465 $this->_openParagraph($pos,$match,$handler); 466 $m1['depth'] = $m0['depth']; 467 $m1 = $m0 + array('p' => 1); 468 469 // restore stack 470 array_push($this->stack, $m1); 471 break; 472 } 473 474 // List item markup 475 if ($m0['depth'] < $m1['depth']) { // list becomes deeper 476 // restore stack 477 array_push($this->stack, $m0); 478 479 } else if ($m0['depth'] == $m1['depth']) { 480 // close item [li|dt|dd] 481 $this->_closeItem($m0, $pos,$match,$handler); 482 // close list [ul|ol|dl] if necessary 483 if ($this->isListTypeChanged($m0, $m1)) { 484 $this->_closeList($m0, $pos,$match,$handler); 485 $m0['num'] = 0; 486 } 487 } 488 489 // open list [ul|ol|dl] if necessary 490 if (($m0['depth'] < $m1['depth']) || (isset($m0['num']) && ($m0['num'] === 0))) { 491 if (isset($m1['num']) && !is_numeric($m1['num'])) $m1['num'] = 1; 492 $this->_openList($m1, $pos,$match,$handler); 493 } else { 494 if (isset($m1['num']) && !is_numeric($m1['num'])) $m1['num'] = $m0['num'] +1; 495 } 496 497 // open item [li|dt|dd] 498 $this->_openItem($m1, $pos,$match,$handler); 499 // open inner wrapper [div|span] 500 $this->_openWrapper($m1, $pos,$match,$handler); 501 // open p if necessary 502 if (isset($m1['p'])) $this->_openParagraph($pos,$match,$handler); 503 504 // add to stack 505 array_push($this->stack, $m1); 506 507 } // end of switch 508 return false; 509 } 510 511 512 /** 513 * Create output 514 */ 515 public function render($format, Doku_Renderer $renderer, $data) 516 { 517 switch ($format) { 518 case 'xhtml': 519 return $this->render_xhtml($renderer, $data); 520 //case 'latex': 521 // $latex = $this->loadHelper('extlist_latex'); 522 // return $latex->render($renderer, $data); 523 //case 'odt': 524 // $odt = $this->loadHelper('extlist_odt'); 525 // return $odt->render($renderer, $data); 526 default: 527 return false; 528 } 529 } 530 531 532 /** 533 * Create xhtml output 534 */ 535 protected function render_xhtml(Doku_Renderer $renderer, $data) 536 { 537 list($state, $tag, $attr) = $data; 538 switch ($state) { 539 case DOKU_LEXER_ENTER: // open tag 540 $renderer->doc.= $this->_open($tag, $attr); 541 break; 542 case DOKU_LEXER_MATCHED: // defensive, shouldn't occur 543 case DOKU_LEXER_UNMATCHED: 544 $renderer->cdata($tag); 545 break; 546 case DOKU_LEXER_EXIT: // close tag 547 $renderer->doc.= $this->_close($tag); 548 break; 549 } 550 return true; 551 } 552 553 /** 554 * open a tag, a utility for render_xhtml() 555 */ 556 protected function _open($tag, $attr = null) 557 { 558 if (!empty($attr)) $attr = ' '.$attr; 559 list($before, $after) = $this->_tag_indent($tag); 560 return $before.'<'.$tag.$attr.'>'.$after; 561 } 562 563 /** 564 * close a tag, a utility for render_xhtml() 565 */ 566 protected function _close($tag) 567 { 568 list($before, $after) = $this->_tag_indent('/'.$tag); 569 return $before.'</'.$tag.'>'.$after; 570 } 571 572 /** 573 * prefix and suffix of html tags 574 * 575 * Initialize this array only once instead of each time the method _tag_indent() 576 * is called. 577 */ 578 protected $indent = [ 579 'ol' => [NL,NL], '/ol' => ['',NL], 580 'ul' => [NL,NL], '/ul' => ['',NL], 581 'dl' => [NL,NL], '/dl' => ['',NL], 582 'li' => [' ',''], '/li' => ['',NL], 583 'dt' => [' ',''], '/dt' => ['',NL], 584 'dd' => [' ',NL], '/dd' => ['',NL], 585 'p' => [NL,''], '/p' => ['',NL], 586 'div' => [NL,''], '/div' => ['',NL], 587 ]; 588 589 /** 590 * indent tags for readability if HTML source 591 * 592 * @param string $tag tag name 593 * @return array 594 */ 595 private function _tag_indent($tag) 596 { 597 if (array_key_exists($tag, $this->indent)) 598 return $this->indent[$tag]; 599 600 return ['','']; 601 } 602 603} 604