1<?php 2/** 3 * Mikio Core Syntax Plugin 4 * 5 * @link http://github.com/nomadjimbob/mikioplugin 6 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 7 * @author James Collins <james.collins@outlook.com.au> 8 */ 9if (!defined('DOKU_INC')) die(); 10if (!defined('DOKU_PLUGIN')) define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/'); 11 12define('MIKIO_LEXER_AUTO', 0); 13define('MIKIO_LEXER_ENTER', 1); 14define('MIKIO_LEXER_EXIT', 2); 15define('MIKIO_LEXER_SPECIAL', 3); 16 17class syntax_plugin_mikioplugin_core extends DokuWiki_Syntax_Plugin 18{ 19 public $pattern_entry = ''; 20 public $pattern = ''; 21 public $pattern_exit = ''; 22 public $tag = ''; 23 public $hasEndTag = true; 24 public $options = array(); 25 26 protected $tagPrefix = ''; //'mikio-'; 27 protected $classPrefix = 'mikiop-'; 28 protected $elemClass = 'mikiop'; 29 30 private $values = array(); 31 32 33 function __construct() { } 34 public function getType() { return 'formatting'; } 35 public function getAllowedTypes() { return array('formatting', 'substition', 'disabled', 'paragraphs'); } 36 // public function getAllowedTypes() { return array('formatting', 'substition', 'disabled'); } 37 public function getSort() { return 32; } 38 public function getPType() { return 'stack'; } 39 40 41 public function connectTo($mode) 42 { 43 if ($this->pattern_entry == '' && $this->tag != '') { 44 if ($this->hasEndTag) { 45 $this->pattern_entry = '<(?i:' . $this->tagPrefix . $this->tag . ')(?=[ >]).*?>(?=.*?</(?i:' . $this->tagPrefix . $this->tag . ')>)'; 46 } else { 47 $this->pattern_entry = '<(?i:' . $this->tagPrefix . $this->tag . ').*?>'; 48 } 49 } 50 51 if ($this->pattern_entry != '') { 52 if ($this->hasEndTag) { 53 $this->Lexer->addEntryPattern($this->pattern_entry, $mode, 'plugin_mikioplugin_' . $this->getPluginComponent()); 54 } else { 55 $this->Lexer->addSpecialPattern($this->pattern_entry, $mode, 'plugin_mikioplugin_' . $this->getPluginComponent()); 56 } 57 } 58 } 59 60 61 public function postConnect() 62 { 63 if ($this->hasEndTag) { 64 if ($this->pattern_exit == '' && $this->tag != '') { 65 $this->pattern_exit = '</(?i:' . $this->tagPrefix . $this->tag . ')>'; 66 } 67 68 if ($this->pattern_exit != '') { 69 $this->Lexer->addExitPattern($this->pattern_exit, 'plugin_mikioplugin_' . $this->getPluginComponent()); 70 } 71 } 72 } 73 74 public function handle($match, $state, $pos, Doku_Handler $handler) 75 { 76 switch ($state) { 77 case DOKU_LEXER_ENTER: 78 case DOKU_LEXER_SPECIAL: 79 $optionlist = preg_split('/\s(?=([^"]*"[^"]*")*[^"]*$)/', trim(substr($match, strlen($this->tagPrefix . $this->tag) + 1, -1))); 80 81 $options = array(); 82 foreach ($optionlist as $item) { 83 $i = strpos($item, '='); 84 if ($i !== false) { 85 $value = substr($item, $i + 1); 86 87 if (substr($value, 0, 1) == '"') $value = substr($value, 1); 88 if (substr($value, -1) == '"') $value = substr($value, 0, -1); 89 90 $options[substr($item, 0, $i)] = $value; 91 } else { 92 $options[$item] = true; 93 } 94 } 95 96 if(count($this->options) > 0) { 97 $options_clean = $this->cleanOptions($options); 98 } else { 99 $options_clean = $options; 100 } 101 102 $this->values = $options_clean; 103 104 return array($state, $options_clean); 105 106 case DOKU_LEXER_MATCHED: 107 return array($state, $match); 108 109 case DOKU_LEXER_UNMATCHED: 110 return array($state, $match); 111 112 case DOKU_LEXER_EXIT: 113 return array($state, $this->values); 114 } 115 116 return array(); 117 } 118 119 120 /* 121 * clean element options to only supported attributes, setting defaults if required 122 * 123 * @param $options options passed to element 124 * @return array of options supported with default set 125 */ 126 protected function cleanOptions($data, $options=null) 127 { 128 $optionsCleaned = array(); 129 130 if($options == null) $options = $this->options; 131 132 // Match DokuWiki passed options to syntax options 133 foreach ($data as $optionKey => $optionValue) { 134 foreach ($options as $syntaxKey => $syntaxValue) { 135 if (strcasecmp($optionKey, $syntaxKey) == 0) { 136 if (array_key_exists('type', $options[$syntaxKey])) { 137 $type = $options[$syntaxKey]['type']; 138 139 switch ($type) { 140 case 'boolean': 141 $optionsCleaned[$syntaxKey] = filter_var($optionValue, FILTER_VALIDATE_BOOLEAN); 142 break; 143 case 'number': 144 $optionsCleaned[$syntaxKey] = filter_var($optionValue, FILTER_VALIDATE_INT); 145 break; 146 case 'float': 147 $optionsCleaned[$syntaxKey] = filter_var($optionValue, FILTER_VALIDATE_FLOAT); 148 break; 149 case 'text': 150 $optionsCleaned[$syntaxKey] = $optionValue; 151 break; 152 case 'size': 153 $s = strtolower($optionValue); 154 $i = ''; 155 if (substr($s, -3) == 'rem') { 156 $i = substr($s, 0, -3); 157 $s = 'rem'; 158 } elseif (substr($s, -2) == 'em') { 159 $i = substr($s, 0, -2); 160 $s = 'em'; 161 } 162 elseif (substr($s, -2) == 'px') { 163 $i = substr($s, 0, -2); 164 $s = 'px'; 165 } 166 elseif (substr($s, -1) == '%') { 167 $i = substr($s, 0, -1); 168 $s = '%'; 169 } 170 else { 171 $i = filter_var($s, FILTER_VALIDATE_INT); 172 if ($i == '') $i = '1'; 173 $s = 'rem'; 174 } 175 176 $optionsCleaned[$syntaxKey] = $i . $s; 177 break; 178 case 'color': 179 if (strlen($optionValue) == 3 || strlen($optionValue) == 6) { 180 preg_match('/([[:xdigit:]]{3}){1,2}/', $optionValue, $matches); 181 if (count($matches) > 1) { 182 $optionsCleaned[$syntaxKey] = '#' . $matches[0]; 183 } 184 } else { 185 $optionsCleaned[$syntaxKey] = $optionValue; 186 } 187 break; 188 case 'url': 189 $optionsCleaned[$syntaxKey] = $this->buildLink($optionValue); 190 break; 191 case 'media': 192 $optionsCleaned[$syntaxKey] = $this->buildMediaLink($optionValue); 193 break; 194 case 'choice': 195 if (array_key_exists('data', $options[$syntaxKey])) { 196 foreach ($options[$syntaxKey]['data'] as $choiceKey => $choiceValue) { 197 if (is_array($choiceValue)) { 198 foreach ($choiceValue as $choiceItem) { 199 if (strcasecmp($optionValue, $choiceItem) == 0) { 200 $optionsCleaned[$syntaxKey] = $choiceKey; 201 break 2; 202 } 203 } 204 } else { 205 if (strcasecmp($optionValue, $choiceValue) == 0) { 206 $optionsCleaned[$syntaxKey] = $choiceValue; 207 break; 208 } 209 } 210 } 211 } 212 break; 213 case 'set': 214 if (array_key_exists('option', $options[$syntaxKey]) && array_key_exists('data', $options[$syntaxKey])) { 215 $optionsCleaned[$options[$syntaxKey]['option']] = $options[$syntaxKey]['data']; 216 } 217 break; 218 } 219 } 220 221 break; 222 } else { 223 if (array_key_exists('type', $options[$syntaxKey]) && $options[$syntaxKey]['type'] == 'choice' && array_key_exists('data', $options[$syntaxKey])) { 224 foreach ($options[$syntaxKey]['data'] as $choiceKey => $choiceValue) { 225 if (is_array($choiceValue)) { 226 foreach ($choiceValue as $choiceItem) { 227 if (strcasecmp($optionKey, $choiceItem) == 0) { 228 $optionsCleaned[$syntaxKey] = $choiceKey; 229 break 2; 230 } 231 } 232 } else { 233 if (strcasecmp($optionKey, $choiceValue) == 0) { 234 $optionsCleaned[$syntaxKey] = $choiceValue; 235 break; 236 } 237 } 238 } 239 } 240 } 241 } 242 } 243 244 // Add in syntax options that are missing 245 foreach ($options as $optionKey => $optionValue) { 246 if (!array_key_exists($optionKey, $optionsCleaned)) { 247 if (array_key_exists('default', $options[$optionKey])) { 248 switch ($options[$optionKey]['type']) { 249 case 'boolean': 250 $optionsCleaned[$optionKey] = filter_var($options[$optionKey]['default'], FILTER_VALIDATE_BOOLEAN); 251 break; 252 case 'number': 253 $optionsCleaned[$optionKey] = filter_var($options[$optionKey]['default'], FILTER_VALIDATE_INT); 254 break; 255 default: 256 $optionsCleaned[$optionKey] = $options[$optionKey]['default']; 257 break; 258 } 259 } 260 } 261 } 262 263 return $optionsCleaned; 264 } 265 266 /* Lexer renderers */ 267 protected function render_lexer_enter(Doku_Renderer $renderer, $data) { } 268 protected function render_lexer_unmatched(Doku_Renderer $renderer, $data) { $renderer->doc .= $renderer->_xmlEntities($data); } 269 protected function render_lexer_exit(Doku_Renderer $renderer, $data) { } 270 protected function render_lexer_special(Doku_Renderer $renderer, $data) { } 271 protected function render_lexer_match(Doku_Renderer $renderer, $data) { } 272 273 /* Renderer */ 274 public function render($mode, Doku_Renderer $renderer, $data) 275 { 276 if ($mode == 'xhtml') { 277 list($state, $match) = $data; 278 279 switch ($state) { 280 case DOKU_LEXER_ENTER: 281 $this->render_lexer_enter($renderer, $match); 282 return true; 283 284 case DOKU_LEXER_UNMATCHED: 285 $this->render_lexer_unmatched($renderer, $match); 286 return true; 287 288 case DOKU_LEXER_MATCHED: 289 $this->render_lexer_match($renderer, $match); 290 return true; 291 292 case DOKU_LEXER_EXIT: 293 $this->render_lexer_exit($renderer, $match); 294 return true; 295 296 case DOKU_LEXER_SPECIAL: 297 $this->render_lexer_special($renderer, $match); 298 return true; 299 } 300 301 return true; 302 } 303 304 return false; 305 } 306 307 /* 308 * return a class list with mikiop- prefix 309 * 310 * @param $options options of syntax element. Options with key 'class'=true are automatically added 311 * @param $classes classes to build from options as array 312 * @param $inclAttr include class="" in the return string 313 * @param $optionsTemplate allow a different options template instead of $this->options (for findTags) 314 * @return a string of classes from options/classes variable 315 */ 316 public function buildClass($options = null, $classes = null, $inclAttr = false, $optionsTemplate = null) 317 { 318 $s = array(); 319 320 if (is_array($options)) { 321 if($classes == null) $classes = array(); 322 if($optionsTemplate == null) $optionsTemplate = $this->options; 323 324 foreach($optionsTemplate as $key => $value) { 325 if(array_key_exists('class', $value) && $value['class'] == TRUE) { 326 array_push($classes, $key); 327 } 328 } 329 330 foreach ($classes as $class) { 331 if (array_key_exists($class, $options) && $options[$class] !== FALSE && $options[$class] != '') { 332 $prefix = $this->classPrefix; 333 334 if (array_key_exists($class, $optionsTemplate) && array_key_exists('prefix', $optionsTemplate[$class])) { 335 $prefix .= $optionsTemplate[$class]['prefix']; 336 } 337 338 if (array_key_exists($class, $optionsTemplate) && array_key_exists('classNoSuffix', $optionsTemplate[$class]) && $optionsTemplate[$class]['classNoSuffix'] == TRUE) { 339 $s[] = $prefix . $class; 340 } else { 341 $s[] = $prefix . $class . ($options[$class] !== TRUE ? '-' . $options[$class] : ''); 342 } 343 } 344 } 345 346 } 347 348 $s = implode(' ', $s); 349 if($s != '') $s = ' ' . $s; 350 351 if($inclAttr) $s = ' classes="' . $s . '"'; 352 353 return $s; 354 } 355 356 357 358 359 /* 360 * build style string 361 * 362 * @param $list style list as key => value. Empty values are not included 363 * @param $inclAttr include style="" in the return string 364 * @return style list string 365 */ 366 public function buildStyle($list, $inclAttr = false) 367 { 368 $s = ''; 369 370 if (is_array($list) && count($list) > 0) { 371 foreach ($list as $key => $value) { 372 if($value != '') { 373 $s .= $key . ':' . $value . ';'; 374 } 375 } 376 } 377 378 if($s != '' && $inclAttr) { 379 $s = ' style="' . $s . '"'; 380 } 381 382 return $s; 383 } 384 385 386 public function buildTooltipString($options) 387 { 388 $dataPlacement = 'top'; 389 $dataHtml = false; 390 $title = ''; 391 392 if ($options != null) { 393 if (array_key_exists('tooltip-html-top', $options) && $options['tooltip-html-top'] != '') { 394 $title = $options['tooltip-html-top']; 395 $dataPlacement = 'top'; 396 } 397 398 if (array_key_exists('tooltip-html-left', $options) && $options['tooltip-html-left'] != '') { 399 $title = $options['tooltip-html-left']; 400 $dataPlacement = 'left'; 401 } 402 403 if (array_key_exists('tooltip-html-bottom', $options) && $options['tooltip-html-bottom'] != '') { 404 $title = $options['tooltip-html-bottom']; 405 $dataPlacement = 'bottom'; 406 } 407 408 if (array_key_exists('tooltip-html-right', $options) && $options['tooltip-html-right'] != '') { 409 $title = $options['tooltip-html-right']; 410 $dataPlacement = 'right'; 411 } 412 413 if (array_key_exists('tooltip-top', $options) && $options['tooltip-top'] != '') { 414 $title = $options['tooltip-top']; 415 $dataPlacement = 'top'; 416 } 417 418 if (array_key_exists('tooltip-left', $options) && $options['tooltip-left'] != '') { 419 $title = $options['tooltip-left']; 420 $dataPlacement = 'left'; 421 } 422 423 if (array_key_exists('tooltip-bottom', $options) && $options['tooltip-bottom'] != '') { 424 $title = $options['tooltip-bottom']; 425 $dataPlacement = 'bottom'; 426 } 427 428 if (array_key_exists('tooltip-right', $options) && $options['tooltip-right'] != '') { 429 $title = $options['tooltip-right']; 430 $dataPlacement = 'right'; 431 } 432 433 if (array_key_exists('tooltip-html', $options) && $options['tooltip-html'] != '') { 434 $title = $options['tooltip-html']; 435 $dataPlacement = 'top'; 436 } 437 438 if (array_key_exists('tooltip', $options) && $options['tooltip'] != '') { 439 $title = $options['tooltip']; 440 $dataPlacement = 'top'; 441 } 442 } 443 444 if ($title != '') { 445 return ' data-toggle="tooltip" data-placement="' . $dataPlacement . '" ' . ($dataHtml == true ? 'data-html="true" ' : '') . 'title="' . $title . '" '; 446 } 447 448 return ''; 449 } 450 451 /* 452 * convert the URL to a DokuWiki media link (if required) 453 * 454 * @param $url url to parse 455 * @return url string 456 */ 457 public function buildMediaLink($url) 458 { 459 $i = strpos($url, '?'); 460 if ($i !== FALSE) $url = substr($url, 0, $i); 461 462 $url = preg_replace('/[^\da-zA-Z:_.]+/', '', $url); 463 464 return (tpl_getMediaFile(array($url), FALSE)); 465 } 466 467 468 /* 469 * returns either a url or dokuwiki link 470 * 471 * @param $url link to build from 472 * @return built link 473 */ 474 public function buildLink($url) 475 { 476 $i = strpos($url, '://'); 477 if ($i !== FALSE || substr($url, 0, 1) == '#') return $url; 478 479 return wl($url); 480 } 481 482 /* 483 * Call syntax renderer of mikio syntax plugin 484 * 485 * @param $renderer DokuWiki renderer object 486 * @param $className mikio syntax class to call 487 * @param $text unmatched text to pass outside of lexer. Only used when $lexer=MIKIO_LEXER_AUTO 488 * @param $data tag options to pass to syntax class. Runs through cleanOptions to validate first 489 * @param $lexer which lexer to call 490 */ 491 public function syntaxRender(Doku_Renderer $renderer, $className, $text, $data = null, $lexer = MIKIO_LEXER_AUTO) 492 { 493 $className = 'syntax_plugin_mikioplugin_'.$className; 494 495 if(class_exists($className)) { 496 $class = new $className; 497 498 if (!is_array($data)) $data = array(); 499 500 if(count($class->options) > 0) { 501 $data = $class->cleanOptions($data); 502 } 503 504 switch($lexer) { 505 case MIKIO_LEXER_AUTO: 506 if ($class->hasEndTag) { 507 if(method_exists($class, 'render_lexer_enter')) $class->render_lexer_enter($renderer, $data); 508 $renderer->doc .= $text; 509 if(method_exists($class, 'render_lexer_exit')) $class->render_lexer_exit($renderer, $data); 510 } else { 511 if(method_exists($class, 'render_lexer_special')) $class->render_lexer_special($renderer, $data); 512 } 513 514 break; 515 case MIKIO_LEXER_ENTER: 516 if(method_exists($class, 'render_lexer_enter')) $class->render_lexer_enter($renderer, $data); 517 break; 518 case MIKIO_LEXER_EXIT: 519 if(method_exists($class, 'render_lexer_exit')) $class->render_lexer_exit($renderer, $data); 520 break; 521 case MIKIO_LEXER_SPECIAL: 522 if(method_exists($class, 'render_lexer_special')) $class->render_lexer_special($renderer, $data); 523 break; 524 } 525 } 526 } 527 528 529 protected function callMikioSyntaxTag($className, $data) { 530 $className = 'syntax_plugin_mikioplugin_'.$className; 531 532 if(class_exists($className)) { 533 $class = new $className; 534 535 if(method_exists($class, 'render_lexer_enter')) $class->call($data); 536 } 537 538 return ''; 539 } 540 541 542 protected function buildTooltip($text) { 543 if($text != '') { 544 return ' data-tooltip="' . $text . '"'; 545 } 546 547 return ''; 548 } 549 550 /* 551 * Create array with passed elements and include them if their values are not empty 552 * 553 * @param ... array items 554 */ 555 protected function arrayRemoveEmpties($items) { 556 $result = array(); 557 558 foreach($items as $key => $value) { 559 if($value != '') { 560 $result[$key] = $value; 561 } 562 } 563 564 return $result; 565 } 566 567 public function getFirstArrayKey($data) 568 { 569 if (!function_exists('array_key_first')) { 570 foreach ($data as $key => $unused) { 571 return $key; 572 } 573 } 574 575 return array_key_first($data); 576 } 577 578 579 /* 580 * add common options to options 581 * 582 * @param $typelist common option to add 583 * @param $options save in options 584 */ 585 public function addCommonOptions($typelist) { 586 $types = explode(' ', $typelist); 587 foreach($types as $type) { 588 if(strcasecmp($type, 'shadow') == 0) { 589 $this->options['shadow'] = array('type' => 'choice', 590 'data' => array('large'=> array('shadow-large', 'shadow-lg'), 'small' => array('shadow-small', 'shadow-sm'), true), 591 'default' => '', 592 'class' => true); 593 } 594 595 if(strcasecmp($type, 'width') == 0) { 596 $this->options['width'] = array('type' => 'size', 597 'default' => ''); 598 } 599 600 if(strcasecmp($type, 'height') == 0) { 601 $this->options['height'] = array('type' => 'size', 602 'default' => ''); 603 } 604 605 if(strcasecmp($type, 'type') == 0) { 606 $this->options['type'] = array('type' => 'choice', 607 'data' => array('primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'), 608 'default' => '', 609 'class' => true); 610 } 611 612 if(strcasecmp($type, 'text-align') == 0) { 613 $this->options['text-align'] = array('type' => 'choice', 614 'data' => array('left' => array('text-left'), 'center' => array('text-center'), 'right' => array('text-right')), 615 'default' => '', 616 'class' => true); 617 } 618 619 if(strcasecmp($type, 'align') == 0) { 620 $this->options['align'] = array('type' => 'choice', 621 'data' => array('left' => array('align-left'), 'center' => array('align-center'), 'right' => array('align-right')), 622 'default' => '', 623 'class' => true); 624 } 625 626 if(strcasecmp($type, 'tooltip') == 0) { 627 $this->options['tooltip'] = array('type' => 'text', 628 'default' => '', 629 'class' => true, 630 'classNoSuffix' => true); 631 } 632 } 633 } 634 635 636 /* 637 * Find HTML tags in string. Parse tags options. Used in parsing subtags 638 * 639 * @param $tagName tagName to search for. Name is exclusive 640 * @param $content search within content 641 * @param $options parse options similar to syntax element options 642 * @param $hasEndTag tagName search also looks for an end tag 643 * @return array of tags containing 'options' => array of 'name' => 'value', 'content' => content inside the tag 644 */ 645 protected function findTags($tagName, $content, $options, $hasEndTag = true) { 646 $items = array(); 647 $search = '/<(?i:' . $tagName . ')(.*?)>(.*?)<\/(?i:' . $tagName . ')>/s'; 648 649 if(!$hasEndTag) { 650 $search = '/<(?i:' . $tagName . ')(.*?)>/s'; 651 } 652 653 if(preg_match_all($search, $content, $match)) { 654 if(count($match) >= 2) { 655 for($i = 0; $i < count($match[1]); $i++) { 656 $item = array('options' => array(), 'content' => $this->render_text($match[2][$i])); 657 658 $optionlist = preg_split('/\s(?=([^"]*"[^"]*")*[^"]*$)/', trim($match[1][$i])); 659 660 foreach ($optionlist as $option) { 661 $j = strpos($option, '='); 662 if ($j !== false) { 663 $value = substr($option, $j + 1); 664 665 if (substr($value, 0, 1) == '"') $value = substr($value, 1); 666 if (substr($value, -1) == '"') $value = substr($value, 0, -1); 667 668 $item['options'][substr($option, 0, $j)] = $value; 669 } else { 670 $item['options'][$option] = true; 671 } 672 } 673 674 $item['options'] = $this->cleanOptions($item['options'], $options); 675 676 $items[] = $item; 677 } 678 } 679 } 680 681 return $items; 682 } 683}