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 ($match[0]) { 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 } 238 // list class 239 $class = 'extlist'; 240 if (isset($this->list_class[$tag])) { 241 // Note: list_class can be empty 242 $class.= ' '.$this->list_class[$tag]; 243 } else { 244 $class.= ' '.$this->getConf($tag.'_class'); 245 } 246 $class = rtrim($class); 247 $attr.= !empty($attr) ? ' ' : ''; 248 $attr.= ' class="'.$class.'"'; 249 $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); 250 } 251 252 /** 253 * write call to close a list block [ul|ol|dl] 254 */ 255 private function _closeList($m, $pos, $match, $handler) 256 { 257 $tag = $m['list']; 258 if ($tag == 'ol') { 259 $this->olist_level--; // reduce olist level 260 } 261 $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); 262 } 263 264 /** 265 * write call to open a list item [li|dt|dd] 266 */ 267 private function _openItem($m, $pos, $match, $handler) 268 { 269 $tag = $m['item']; 270 switch ($m['mk']) { 271 case '-': 272 case '-:': 273 // prepare hierarchical marker for nested ordered list item 274 $this->olist_info[$this->olist_level] = $m['num']; 275 $lv = $this->olist_level; 276 $attr = ' value="'.$m['num'].'"'; 277 $attr.= ' data-marker="'.$this->olist_marker($lv).'"'; 278 break; 279 case ';': 280 $attr = 'class="'.$m['class'].'"'; 281 break; 282 default: 283 $attr = ''; 284 } 285 $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); 286 } 287 288 /** 289 * write call to close a list item [li|dt|dd] 290 */ 291 private function _closeItem($m, $pos, $match, $handler) 292 { 293 $tag = $m['item']; 294 $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); 295 } 296 297 /** 298 * write call to open inner wrapper [div|span] 299 */ 300 private function _openWrapper($m, $pos, $match, $handler) 301 { 302 switch ($m['mk']) { 303 case ';': // dl dt 304 case ';;': // dl dt, explicitly no-compact 305 $tag = 'span'; $attr = ''; 306 break; 307 case ':': // dl dd 308 case '::': // dl dd p 309 return; 310 break; 311 default: 312 if (!$this->use_div) return; 313 $tag = 'div'; $attr = 'class="li"'; 314 break; 315 } 316 $this->_writeCall($tag,$attr,DOKU_LEXER_ENTER, $pos,$match,$handler); 317 } 318 319 /** 320 * write call to close inner wrapper [div|span] 321 */ 322 private function _closeWrapper($m, $pos, $match, $handler) 323 { 324 switch ($m['mk']) { 325 case ';': // dl dt 326 case ';;': // dl dt, explicitly no-compact 327 $tag = 'span'; 328 break; 329 case ':': // dl dd 330 case '::': // dl dd p 331 return; 332 break; 333 default: 334 if (!$this->use_div) return; 335 $tag = 'div'; 336 break; 337 } 338 $this->_writeCall($tag,'',DOKU_LEXER_EXIT, $pos,$match,$handler); 339 } 340 341 /** 342 * write call to open paragraph (p tag) 343 */ 344 private function _openParagraph($pos, $match, $handler) 345 { 346 $this->_writeCall('p','',DOKU_LEXER_ENTER, $pos,$match,$handler); 347 } 348 349 /** 350 * write call to close paragraph (p tag) 351 */ 352 private function _closeParagraph($pos, $match, $handler) 353 { 354 $this->_writeCall('p','',DOKU_LEXER_EXIT, $pos,$match,$handler); 355 } 356 357 /** 358 * Handle the match 359 */ 360 public function handle($match, $state, $pos, Doku_Handler $handler) 361 { 362 switch ($state) { 363 case DOKU_LEXER_SPECIAL: 364 // specify class attribute for lists [ul|ol|dl] 365 $this->storeListClass($match); 366 break; 367 368 case DOKU_LEXER_ENTER: 369 $m1 = $this->interpret($match); 370 if (($m1['list'] == 'ol') && !isset($m1['num'])) { 371 $m1['num'] = 1; 372 } 373 374 // open list tag [ul|ol|dl] 375 $this->_openList($m1, $pos,$match,$handler); 376 // open item tag [li|dt|dd] 377 $this->_openItem($m1, $pos,$match,$handler); 378 // open inner wrapper [div|span] 379 $this->_openWrapper($m1, $pos,$match,$handler); 380 // open p if necessary 381 if (isset($m1['p'])) $this->_openParagraph($pos,$match,$handler); 382 383 // add to stack 384 array_push($this->stack, $m1); 385 break; 386 387 case DOKU_LEXER_UNMATCHED: 388 // cdata --- use base() as _writeCall() is prefixed for private/protected 389 $handler->base($match, $state, $pos); 390 break; 391 392 case DOKU_LEXER_EXIT: 393 // clear list_class 394 $this->list_class = array(); 395 // do not break here! 396 397 case DOKU_LEXER_MATCHED: 398 // specify class attribute for lists [ul|ol|dl] 399 if (substr($match, -2) == '~~') { 400 $this->storeListClass($match); 401 break; 402 } 403 404 // retrieve previous list item from stack 405 $m0 = array_pop($this->stack); 406 $m1 = $this->interpret($match); 407 408 // set m1 depth if dt and dd are in one line 409 if (($m1['depth'] == 0) && ($m0['item'] == 'dt')) { 410 $m1['depth'] = $m0['depth']; 411 } 412 413 // continued list item content, indented by at least two spaces 414 if (empty($m1['mk']) && ($m1['depth'] > 0)) { 415 // !!EXPERIMENTAL SCRIPTIO CONTINUA concerns!! 416 // replace indent to single space, but leave it for LineBreak2 plugin 417 $handler->base("\n", DOKU_LEXER_UNMATCHED, $pos); 418 419 // restore stack 420 array_push($this->stack, $m0); 421 break; 422 } 423 424 // close p if necessary 425 if (isset($m0['p'])) $this->_closeParagraph($pos,$match,$handler); 426 427 // close inner wrapper [div|span] if necessary 428 if ($m1['mk'] == '+:') { 429 // Paragraph markup 430 if ($m0['depth'] > $m1['depth']) { 431 $this->_closeWrapper($m0, $pos,$match,$handler); 432 } else { 433 // new paragraph can not be deeper than previous depth 434 // fix current depth quietly 435 $m1['depth'] = min($m0['depth'], $m1['depth']); 436 } 437 // fix previous p type 438 $m0['p'] = 1; 439 } else { 440 // List item markup 441 if ($m0['depth'] >= $m1['depth']) { 442 $this->_closeWrapper($m0, $pos,$match,$handler); 443 } 444 } 445 446 // List item becomes shallower - close deeper list 447 while ($m0['depth'] > $m1['depth']) { 448 // close item [li|dt|dd] 449 $this->_closeItem($m0, $pos,$match,$handler); 450 // close list [ul|ol|dl] 451 $this->_closeList($m0, $pos,$match,$handler); 452 453 $m0 = array_pop($this->stack); 454 } 455 456 // Break out of switch structure if end of list block 457 if ($state == DOKU_LEXER_EXIT) { 458 break; 459 } 460 461 // Paragraph markup 462 if ($m1['mk'] == '+:') { 463 $this->_openParagraph($pos,$match,$handler); 464 $m1['depth'] = $m0['depth']; 465 $m1 = $m0 + array('p' => 1); 466 467 // restore stack 468 array_push($this->stack, $m1); 469 break; 470 } 471 472 // List item markup 473 if ($m0['depth'] < $m1['depth']) { // list becomes deeper 474 // restore stack 475 array_push($this->stack, $m0); 476 477 } else if ($m0['depth'] == $m1['depth']) { 478 // close item [li|dt|dd] 479 $this->_closeItem($m0, $pos,$match,$handler); 480 // close list [ul|ol|dl] if necessary 481 if ($this->isListTypeChanged($m0, $m1)) { 482 $this->_closeList($m0, $pos,$match,$handler); 483 $m0['num'] = 0; 484 } 485 } 486 487 // open list [ul|ol|dl] if necessary 488 if (($m0['depth'] < $m1['depth']) || ($m0['num'] === 0)) { 489 if (!is_numeric($m1['num'])) $m1['num'] = 1; 490 $this->_openList($m1, $pos,$match,$handler); 491 } else { 492 if (!is_numeric($m1['num'])) $m1['num'] = $m0['num'] +1; 493 } 494 495 // open item [li|dt|dd] 496 $this->_openItem($m1, $pos,$match,$handler); 497 // open inner wrapper [div|span] 498 $this->_openWrapper($m1, $pos,$match,$handler); 499 // open p if necessary 500 if (isset($m1['p'])) $this->_openParagraph($pos,$match,$handler); 501 502 // add to stack 503 array_push($this->stack, $m1); 504 505 } // end of switch 506 return false; 507 } 508 509 510 /** 511 * Create output 512 */ 513 public function render($format, Doku_Renderer $renderer, $data) 514 { 515 switch ($format) { 516 case 'xhtml': 517 return $this->render_xhtml($renderer, $data); 518 //case 'latex': 519 // $latex = $this->loadHelper('extlist_latex'); 520 // return $latex->render($renderer, $data); 521 //case 'odt': 522 // $odt = $this->loadHelper('extlist_odt'); 523 // return $odt->render($renderer, $data); 524 default: 525 return false; 526 } 527 } 528 529 530 /** 531 * Create xhtml output 532 */ 533 protected function render_xhtml(Doku_Renderer $renderer, $data) 534 { 535 list($state, $tag, $attr) = $data; 536 switch ($state) { 537 case DOKU_LEXER_ENTER: // open tag 538 $renderer->doc.= $this->_open($tag, $attr); 539 break; 540 case DOKU_LEXER_MATCHED: // defensive, shouldn't occur 541 case DOKU_LEXER_UNMATCHED: 542 $renderer->cdata($tag); 543 break; 544 case DOKU_LEXER_EXIT: // close tag 545 $renderer->doc.= $this->_close($tag); 546 break; 547 } 548 return true; 549 } 550 551 /** 552 * open a tag, a utility for render_xhtml() 553 */ 554 protected function _open($tag, $attr = null) 555 { 556 if (!empty($attr)) $attr = ' '.$attr; 557 list($before, $after) = $this->_tag_indent($tag); 558 return $before.'<'.$tag.$attr.'>'.$after; 559 } 560 561 /** 562 * close a tag, a utility for render_xhtml() 563 */ 564 protected function _close($tag) 565 { 566 list($before, $after) = $this->_tag_indent('/'.$tag); 567 return $before.'</'.$tag.'>'.$after; 568 } 569 570 /** 571 * indent tags for readability if HTML source 572 * 573 * @param string $tag tag name 574 * @return array 575 */ 576 private function _tag_indent($tag) 577 { 578 // prefix and surffix of html tags 579 $indent = array( 580 'ol' => array("\n","\n"), '/ol' => array("","\n"), 581 'ul' => array("\n","\n"), '/ul' => array("","\n"), 582 'dl' => array("\n","\n"), '/dl' => array("","\n"), 583 'li' => array(" ",""), '/li' => array("","\n"), 584 'dt' => array(" ",""), '/dt' => array("","\n"), 585 'dd' => array(" ","\n"), '/dd' => array("","\n"), 586 'p' => array("\n",""), '/p' => array("","\n"), 587 ); 588 return $indent[$tag]; 589 } 590 591} 592