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