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 $s[] = $prefix . $class . ($options[$class] !== TRUE ? '-' . $options[$class] : ''); 339 } 340 } 341 342 } 343 344 $s = implode(' ', $s); 345 if($s != '') $s = ' ' . $s; 346 347 if($inclAttr) $s = ' classes="' . $s . '"'; 348 349 return $s; 350 } 351 352 353 354 355 /* 356 * build style string 357 * 358 * @param $list style list as key => value. Empty values are not included 359 * @param $inclAttr include style="" in the return string 360 * @return style list string 361 */ 362 public function buildStyle($list, $inclAttr = false) 363 { 364 $s = ''; 365 366 if (is_array($list) && count($list) > 0) { 367 foreach ($list as $key => $value) { 368 if($value != '') { 369 $s .= $key . ':' . $value . ';'; 370 } 371 } 372 } 373 374 if($s != '' && $inclAttr) { 375 $s = ' style="' . $s . '"'; 376 } 377 378 return $s; 379 } 380 381 382 public function buildTooltipString($options) 383 { 384 $dataPlacement = 'top'; 385 $dataHtml = false; 386 $title = ''; 387 388 if ($options != null) { 389 if (array_key_exists('tooltip-html-top', $options) && $options['tooltip-html-top'] != '') { 390 $title = $options['tooltip-html-top']; 391 $dataPlacement = 'top'; 392 } 393 394 if (array_key_exists('tooltip-html-left', $options) && $options['tooltip-html-left'] != '') { 395 $title = $options['tooltip-html-left']; 396 $dataPlacement = 'left'; 397 } 398 399 if (array_key_exists('tooltip-html-bottom', $options) && $options['tooltip-html-bottom'] != '') { 400 $title = $options['tooltip-html-bottom']; 401 $dataPlacement = 'bottom'; 402 } 403 404 if (array_key_exists('tooltip-html-right', $options) && $options['tooltip-html-right'] != '') { 405 $title = $options['tooltip-html-right']; 406 $dataPlacement = 'right'; 407 } 408 409 if (array_key_exists('tooltip-top', $options) && $options['tooltip-top'] != '') { 410 $title = $options['tooltip-top']; 411 $dataPlacement = 'top'; 412 } 413 414 if (array_key_exists('tooltip-left', $options) && $options['tooltip-left'] != '') { 415 $title = $options['tooltip-left']; 416 $dataPlacement = 'left'; 417 } 418 419 if (array_key_exists('tooltip-bottom', $options) && $options['tooltip-bottom'] != '') { 420 $title = $options['tooltip-bottom']; 421 $dataPlacement = 'bottom'; 422 } 423 424 if (array_key_exists('tooltip-right', $options) && $options['tooltip-right'] != '') { 425 $title = $options['tooltip-right']; 426 $dataPlacement = 'right'; 427 } 428 429 if (array_key_exists('tooltip-html', $options) && $options['tooltip-html'] != '') { 430 $title = $options['tooltip-html']; 431 $dataPlacement = 'top'; 432 } 433 434 if (array_key_exists('tooltip', $options) && $options['tooltip'] != '') { 435 $title = $options['tooltip']; 436 $dataPlacement = 'top'; 437 } 438 } 439 440 if ($title != '') { 441 return ' data-toggle="tooltip" data-placement="' . $dataPlacement . '" ' . ($dataHtml == true ? 'data-html="true" ' : '') . 'title="' . $title . '" '; 442 } 443 444 return ''; 445 } 446 447 /* 448 * convert the URL to a DokuWiki media link (if required) 449 * 450 * @param $url url to parse 451 * @return url string 452 */ 453 public function buildMediaLink($url) 454 { 455 $i = strpos($url, '?'); 456 if ($i !== FALSE) $url = substr($url, 0, $i); 457 458 $url = preg_replace('/[^\da-zA-Z:_.]+/', '', $url); 459 460 return (tpl_getMediaFile(array($url), FALSE)); 461 } 462 463 464 /* 465 * returns either a url or dokuwiki link 466 * 467 * @param $url link to build from 468 * @return built link 469 */ 470 public function buildLink($url) 471 { 472 $i = strpos($url, '://'); 473 if ($i !== FALSE || substr($url, 0, 1) == '#') return $url; 474 475 return wl($url); 476 } 477 478 479 480 481 482 483 /* 484 * Call syntax renderer of mikio syntax plugin 485 * 486 * @param $renderer DokuWiki renderer object 487 * @param $className mikio syntax class to call 488 * @param $text unmatched text to pass outside of lexer. Only used when $lexer=MIKIO_LEXER_AUTO 489 * @param $data tag options to pass to syntax class. Runs through cleanOptions to validate first 490 * @param $lexer which lexer to call 491 */ 492 public function syntaxRender(Doku_Renderer $renderer, $className, $text, $data = null, $lexer = MIKIO_LEXER_AUTO) 493 { 494 $className = 'syntax_plugin_mikioplugin_'.$className; 495 496 if(class_exists($className)) { 497 $class = new $className; 498 499 if (!is_array($data)) $data = array(); 500 501 if(count($class->options) > 0) { 502 $data = $class->cleanOptions($data); 503 } 504 505 switch($lexer) { 506 case MIKIO_LEXER_AUTO: 507 if ($class->hasEndTag) { 508 if(method_exists($class, 'render_lexer_enter')) $class->render_lexer_enter($renderer, $data); 509 $renderer->doc .= $text; 510 if(method_exists($class, 'render_lexer_exit')) $class->render_lexer_exit($renderer, $data); 511 } else { 512 if(method_exists($class, 'render_lexer_special')) $class->render_lexer_special($renderer, $data); 513 } 514 515 break; 516 case MIKIO_LEXER_ENTER: 517 if(method_exists($class, 'render_lexer_enter')) $class->render_lexer_enter($renderer, $data); 518 break; 519 case MIKIO_LEXER_EXIT: 520 if(method_exists($class, 'render_lexer_exit')) $class->render_lexer_exit($renderer, $data); 521 break; 522 case MIKIO_LEXER_SPECIAL: 523 if(method_exists($class, 'render_lexer_special')) $class->render_lexer_special($renderer, $data); 524 break; 525 } 526 } 527 } 528 529 /* 530 * Create array with passed elements and include them if their values are not empty 531 * 532 * @param ... array items 533 */ 534 protected function arrayRemoveEmpties($items) { 535 $result = array(); 536 537 foreach($items as $key => $value) { 538 if($value != '') { 539 $result[$key] = $value; 540 } 541 } 542 543 return $result; 544 } 545 546 public function getFirstArrayKey($data) 547 { 548 if (!function_exists('array_key_first')) { 549 foreach ($data as $key => $unused) { 550 return $key; 551 } 552 } 553 554 return array_key_first($data); 555 } 556 557 558 /* 559 * add common options to options 560 * 561 * @param $typelist common option to add 562 * @param $options save in options 563 */ 564 public function addCommonOptions($typelist) { 565 $types = explode(' ', $typelist); 566 foreach($types as $type) { 567 if(strcasecmp($type, 'shadow') == 0) { 568 $this->options['shadow'] = array('type' => 'choice', 569 'data' => array('large'=> array('shadow-large', 'shadow-lg'), 'small' => array('shadow-small', 'shadow-sm'), true), 570 'default' => '', 571 'class' => true); 572 } 573 574 if(strcasecmp($type, 'width') == 0) { 575 $this->options['width'] = array('type' => 'size', 576 'default' => ''); 577 } 578 579 if(strcasecmp($type, 'height') == 0) { 580 $this->options['height'] = array('type' => 'size', 581 'default' => ''); 582 } 583 584 if(strcasecmp($type, 'type') == 0) { 585 $this->options['type'] = array('type' => 'choice', 586 'data' => array('primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'), 587 'default' => '', 588 'class' => true); 589 } 590 591 if(strcasecmp($type, 'text-align') == 0) { 592 $this->options['text-align'] = array('type' => 'choice', 593 'data' => array('left' => array('text-left'), 'center' => array('text-center'), 'right' => array('text-right')), 594 'default' => '', 595 'class' => true); 596 } 597 598 if(strcasecmp($type, 'align') == 0) { 599 $this->options['align'] = array('type' => 'choice', 600 'data' => array('left' => array('align-left'), 'center' => array('align-center'), 'right' => array('align-right')), 601 'default' => '', 602 'class' => true); 603 } 604 } 605 } 606 607 608 /* 609 * Find HTML tags in string. Parse tags options. Used in parsing subtags 610 * 611 * @param $tagName tagName to search for. Name is exclusive 612 * @param $content search within content 613 * @param $options parse options similar to syntax element options 614 * @param $hasEndTag tagName search also looks for an end tag 615 * @return array of tags containing 'options' => array of 'name' => 'value', 'content' => content inside the tag 616 */ 617 protected function findTags($tagName, $content, $options, $hasEndTag = true) { 618 $items = array(); 619 $search = '/<(?i:' . $tagName . ')(.*?)>(.*?)<\/(?i:' . $tagName . ')>/s'; 620 621 if(!$hasEndTag) { 622 $search = '/<(?i:' . $tagName . ')(.*?)>/s'; 623 } 624 625 if(preg_match_all($search, $content, $match)) { 626 if(count($match) >= 2) { 627 for($i = 0; $i < count($match[1]); $i++) { 628 $item = array('options' => array(), 'content' => $this->render_text($match[2][$i])); 629 630 $optionlist = preg_split('/\s(?=([^"]*"[^"]*")*[^"]*$)/', trim($match[1][$i])); 631 632 foreach ($optionlist as $option) { 633 $j = strpos($option, '='); 634 if ($j !== false) { 635 $value = substr($option, $j + 1); 636 637 if (substr($value, 0, 1) == '"') $value = substr($value, 1); 638 if (substr($value, -1) == '"') $value = substr($value, 0, -1); 639 640 $item['options'][substr($option, 0, $j)] = $value; 641 } else { 642 $item['options'][$option] = true; 643 } 644 } 645 646 $item['options'] = $this->cleanOptions($item['options'], $options); 647 648 $items[] = $item; 649 } 650 } 651 } 652 653 return $items; 654 } 655}