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 $match_fix = preg_replace('/\s*=\s*/', '=', trim(substr($match, strlen($this->tagPrefix . $this->tag) + 1, -1))); 80 $optionlist = preg_split('/\s(?=([^"]*"[^"]*")*[^"]*$)/', $match_fix); 81 82 $options = array(); 83 foreach ($optionlist as $item) { 84 $i = strpos($item, '='); 85 if ($i !== false) { 86 $value = substr($item, $i + 1); 87 88 if (substr($value, 0, 1) == '"') $value = substr($value, 1); 89 if (substr($value, -1) == '"') $value = substr($value, 0, -1); 90 91 $options[substr($item, 0, $i)] = $value; 92 } else { 93 $options[$item] = true; 94 } 95 } 96 97 if(count($this->options) > 0) { 98 $options_clean = $this->cleanOptions($options); 99 } else { 100 $options_clean = $options; 101 } 102 103 $this->values = $options_clean; 104 105 return array($state, $options_clean); 106 107 case DOKU_LEXER_MATCHED: 108 return array($state, $match); 109 110 case DOKU_LEXER_UNMATCHED: 111 return array($state, $match); 112 113 case DOKU_LEXER_EXIT: 114 return array($state, $this->values); 115 } 116 117 return array(); 118 } 119 120 121 /* 122 * clean element options to only supported attributes, setting defaults if required 123 * 124 * @param $options options passed to element 125 * @return array of options supported with default set 126 */ 127 protected function cleanOptions($data, $options=null) 128 { 129 $optionsCleaned = array(); 130 131 if($options == null) $options = $this->options; 132 133 // Match DokuWiki passed options to syntax options 134 foreach ($data as $optionKey => $optionValue) { 135 foreach ($options as $syntaxKey => $syntaxValue) { 136 if (strcasecmp($optionKey, $syntaxKey) == 0) { 137 if (array_key_exists('type', $options[$syntaxKey])) { 138 $type = $options[$syntaxKey]['type']; 139 140 switch ($type) { 141 case 'boolean': 142 $optionsCleaned[$syntaxKey] = filter_var($optionValue, FILTER_VALIDATE_BOOLEAN); 143 break; 144 case 'number': 145 $optionsCleaned[$syntaxKey] = filter_var($optionValue, FILTER_VALIDATE_INT); 146 break; 147 case 'float': 148 $optionsCleaned[$syntaxKey] = filter_var($optionValue, FILTER_VALIDATE_FLOAT); 149 break; 150 case 'text': 151 $optionsCleaned[$syntaxKey] = $optionValue; 152 break; 153 case 'size': 154 $s = strtolower($optionValue); 155 $i = ''; 156 if (substr($s, -3) == 'rem') { 157 $i = substr($s, 0, -3); 158 $s = 'rem'; 159 } elseif (substr($s, -2) == 'em') { 160 $i = substr($s, 0, -2); 161 $s = 'em'; 162 } 163 elseif (substr($s, -2) == 'px') { 164 $i = substr($s, 0, -2); 165 $s = 'px'; 166 } 167 elseif (substr($s, -1) == '%') { 168 $i = substr($s, 0, -1); 169 $s = '%'; 170 } 171 else { 172 if($s != 'auto') { 173 $i = filter_var($s, FILTER_VALIDATE_INT); 174 if ($i == '') $i = '1'; 175 $s = 'rem'; 176 } 177 } 178 179 $optionsCleaned[$syntaxKey] = $i . $s; 180 break; 181 case 'multisize': 182 $val = ''; 183 $parts = explode(' ', $optionValue); 184 foreach($parts as &$part) { 185 $s = strtolower($part); 186 $i = ''; 187 if (substr($s, -3) == 'rem') { 188 $i = substr($s, 0, -3); 189 $s = 'rem'; 190 } elseif (substr($s, -2) == 'em') { 191 $i = substr($s, 0, -2); 192 $s = 'em'; 193 } 194 elseif (substr($s, -2) == 'px') { 195 $i = substr($s, 0, -2); 196 $s = 'px'; 197 } 198 elseif (substr($s, -2) == 'fr') { 199 $i = substr($s, 0, -2); 200 $s = 'fr'; 201 } 202 elseif (substr($s, -1) == '%') { 203 $i = substr($s, 0, -1); 204 $s = '%'; 205 } 206 else { 207 if($s != 'auto') { 208 $i = filter_var($s, FILTER_VALIDATE_INT); 209 if ($i === '') $i = '1'; 210 if ($i != 0) { 211 $s = 'rem'; 212 } else { 213 $s = ''; 214 } 215 } 216 } 217 218 $part = $i . $s; 219 } 220 221 $optionsCleaned[$syntaxKey] = implode(' ' , $parts); 222 break; 223 case 'color': 224 if (strlen($optionValue) == 3 || strlen($optionValue) == 6) { 225 preg_match('/([[:xdigit:]]{3}){1,2}/', $optionValue, $matches); 226 if (count($matches) > 1) { 227 $optionsCleaned[$syntaxKey] = '#' . $matches[0]; 228 } 229 } else { 230 $optionsCleaned[$syntaxKey] = $optionValue; 231 } 232 break; 233 case 'url': 234 $optionsCleaned[$syntaxKey] = $this->buildLink($optionValue); 235 break; 236 case 'media': 237 $optionsCleaned[$syntaxKey] = $this->buildMediaLink($optionValue); 238 break; 239 case 'choice': 240 if (array_key_exists('data', $options[$syntaxKey])) { 241 foreach ($options[$syntaxKey]['data'] as $choiceKey => $choiceValue) { 242 if (strcasecmp($optionValue, $choiceKey) == 0) { 243 $optionsCleaned[$syntaxKey] = $choiceKey; 244 break; 245 } 246 247 if (is_array($choiceValue)) { 248 foreach ($choiceValue as $choiceItem) { 249 if (strcasecmp($optionValue, $choiceItem) == 0) { 250 $optionsCleaned[$syntaxKey] = $choiceKey; 251 break 2; 252 } 253 } 254 } else { 255 if (strcasecmp($optionValue, $choiceValue) == 0) { 256 $optionsCleaned[$syntaxKey] = $choiceValue; 257 break; 258 } 259 } 260 } 261 } 262 break; 263 case 'set': 264 if (array_key_exists('option', $options[$syntaxKey]) && array_key_exists('data', $options[$syntaxKey])) { 265 $optionsCleaned[$options[$syntaxKey]['option']] = $options[$syntaxKey]['data']; 266 } 267 break; 268 } 269 } 270 271 break; 272 } 273 } 274 } 275 276 277 foreach ($data as $optionKey => $optionValue) { 278 if (!array_key_exists($optionKey, $optionsCleaned)) { 279 foreach ($options as $syntaxKey => $syntaxValue) { 280 if (array_key_exists('type', $options[$syntaxKey])) { 281 if (array_key_exists('data', $options[$syntaxKey]) && is_array($options[$syntaxKey][data])) { 282 foreach ($options[$syntaxKey]['data'] as $choiceKey => $choiceValue) { 283 if(is_array($choiceValue)) { 284 if(in_array($optionKey, $choiceValue)) { 285 $optionsCleaned[$syntaxKey] = $choiceKey; 286 } 287 } else { 288 if(strcasecmp($choiceValue, $optionKey) == 0) { 289 $optionsCleaned[$syntaxKey] = $choiceValue; 290 } 291 } 292 } 293 } 294 } 295 } 296 } 297 } 298 299 // Add in syntax options that are missing 300 foreach ($options as $optionKey => $optionValue) { 301 if (!array_key_exists($optionKey, $optionsCleaned)) { 302 if (array_key_exists('default', $options[$optionKey])) { 303 switch ($options[$optionKey]['type']) { 304 case 'boolean': 305 $optionsCleaned[$optionKey] = filter_var($options[$optionKey]['default'], FILTER_VALIDATE_BOOLEAN); 306 break; 307 case 'number': 308 $optionsCleaned[$optionKey] = filter_var($options[$optionKey]['default'], FILTER_VALIDATE_INT); 309 break; 310 default: 311 $optionsCleaned[$optionKey] = $options[$optionKey]['default']; 312 break; 313 } 314 } 315 } 316 } 317 318 return $optionsCleaned; 319 } 320 321 /* Lexer renderers */ 322 protected function render_lexer_enter(Doku_Renderer $renderer, $data) { } 323 protected function render_lexer_unmatched(Doku_Renderer $renderer, $data) { $renderer->doc .= $renderer->_xmlEntities($data); } 324 protected function render_lexer_exit(Doku_Renderer $renderer, $data) { } 325 protected function render_lexer_special(Doku_Renderer $renderer, $data) { } 326 protected function render_lexer_match(Doku_Renderer $renderer, $data) { } 327 328 /* Renderer */ 329 public function render($mode, Doku_Renderer $renderer, $data) 330 { 331 if ($mode == 'xhtml') { 332 list($state, $match) = $data; 333 334 switch ($state) { 335 case DOKU_LEXER_ENTER: 336 $this->render_lexer_enter($renderer, $match); 337 return true; 338 339 case DOKU_LEXER_UNMATCHED: 340 $this->render_lexer_unmatched($renderer, $match); 341 return true; 342 343 case DOKU_LEXER_MATCHED: 344 $this->render_lexer_match($renderer, $match); 345 return true; 346 347 case DOKU_LEXER_EXIT: 348 $this->render_lexer_exit($renderer, $match); 349 return true; 350 351 case DOKU_LEXER_SPECIAL: 352 $this->render_lexer_special($renderer, $match); 353 return true; 354 } 355 356 return true; 357 } 358 359 return false; 360 } 361 362 /* 363 * return a class list with mikiop- prefix 364 * 365 * @param $options options of syntax element. Options with key 'class'=true are automatically added 366 * @param $classes classes to build from options as array 367 * @param $inclAttr include class="" in the return string 368 * @param $optionsTemplate allow a different options template instead of $this->options (for findTags) 369 * @return a string of classes from options/classes variable 370 */ 371 public function buildClass($options = null, $classes = null, $inclAttr = false, $optionsTemplate = null) 372 { 373 $s = array(); 374 375 if (is_array($options)) { 376 if($classes == null) $classes = array(); 377 if($optionsTemplate == null) $optionsTemplate = $this->options; 378 379 foreach($optionsTemplate as $key => $value) { 380 if(array_key_exists('class', $value) && $value['class'] == TRUE) { 381 array_push($classes, $key); 382 } 383 } 384 385 foreach ($classes as $class) { 386 if (array_key_exists($class, $options) && $options[$class] !== FALSE && $options[$class] != '') { 387 $prefix = $this->classPrefix; 388 389 if (array_key_exists($class, $optionsTemplate) && array_key_exists('prefix', $optionsTemplate[$class])) { 390 $prefix .= $optionsTemplate[$class]['prefix']; 391 } 392 393 if (array_key_exists($class, $optionsTemplate) && array_key_exists('classNoSuffix', $optionsTemplate[$class]) && $optionsTemplate[$class]['classNoSuffix'] == TRUE) { 394 $s[] = $prefix . $class; 395 } else { 396 $s[] = $prefix . $class . ($options[$class] !== TRUE ? '-' . $options[$class] : ''); 397 } 398 } 399 } 400 401 } 402 403 $s = implode(' ', $s); 404 if($s != '') $s = ' ' . $s; 405 406 if($inclAttr) $s = ' classes="' . $s . '"'; 407 408 return $s; 409 } 410 411 412 413 414 /* 415 * build style string 416 * 417 * @param $list style list as key => value. Empty values are not included 418 * @param $inclAttr include style="" in the return string 419 * @return style list string 420 */ 421 public function buildStyle($list, $inclAttr = false) 422 { 423 $s = ''; 424 425 if (is_array($list) && count($list) > 0) { 426 foreach ($list as $key => $value) { 427 if($value != '') { 428 $s .= $key . ':' . $value . ';'; 429 } 430 } 431 } 432 433 if($s != '' && $inclAttr) { 434 $s = ' style="' . $s . '"'; 435 } 436 437 return $s; 438 } 439 440 441 public function buildTooltipString($options) 442 { 443 $dataPlacement = 'top'; 444 $dataHtml = false; 445 $title = ''; 446 447 if ($options != null) { 448 if (array_key_exists('tooltip-html-top', $options) && $options['tooltip-html-top'] != '') { 449 $title = $options['tooltip-html-top']; 450 $dataPlacement = 'top'; 451 } 452 453 if (array_key_exists('tooltip-html-left', $options) && $options['tooltip-html-left'] != '') { 454 $title = $options['tooltip-html-left']; 455 $dataPlacement = 'left'; 456 } 457 458 if (array_key_exists('tooltip-html-bottom', $options) && $options['tooltip-html-bottom'] != '') { 459 $title = $options['tooltip-html-bottom']; 460 $dataPlacement = 'bottom'; 461 } 462 463 if (array_key_exists('tooltip-html-right', $options) && $options['tooltip-html-right'] != '') { 464 $title = $options['tooltip-html-right']; 465 $dataPlacement = 'right'; 466 } 467 468 if (array_key_exists('tooltip-top', $options) && $options['tooltip-top'] != '') { 469 $title = $options['tooltip-top']; 470 $dataPlacement = 'top'; 471 } 472 473 if (array_key_exists('tooltip-left', $options) && $options['tooltip-left'] != '') { 474 $title = $options['tooltip-left']; 475 $dataPlacement = 'left'; 476 } 477 478 if (array_key_exists('tooltip-bottom', $options) && $options['tooltip-bottom'] != '') { 479 $title = $options['tooltip-bottom']; 480 $dataPlacement = 'bottom'; 481 } 482 483 if (array_key_exists('tooltip-right', $options) && $options['tooltip-right'] != '') { 484 $title = $options['tooltip-right']; 485 $dataPlacement = 'right'; 486 } 487 488 if (array_key_exists('tooltip-html', $options) && $options['tooltip-html'] != '') { 489 $title = $options['tooltip-html']; 490 $dataPlacement = 'top'; 491 } 492 493 if (array_key_exists('tooltip', $options) && $options['tooltip'] != '') { 494 $title = $options['tooltip']; 495 $dataPlacement = 'top'; 496 } 497 } 498 499 if ($title != '') { 500 return ' data-toggle="tooltip" data-placement="' . $dataPlacement . '" ' . ($dataHtml == true ? 'data-html="true" ' : '') . 'title="' . $title . '" '; 501 } 502 503 return ''; 504 } 505 506 /* 507 * convert the URL to a DokuWiki media link (if required) 508 * 509 * @param $url url to parse 510 * @return url string 511 */ 512 public function buildMediaLink($url) 513 { 514 $i = strpos($url, '?'); 515 if ($i !== FALSE) $url = substr($url, 0, $i); 516 517 $url = preg_replace('/[^\da-zA-Z:_.-]+/', '', $url); 518 519 return (tpl_getMediaFile(array($url), FALSE)); 520 } 521 522 523 /* 524 * returns either a url or dokuwiki link 525 * 526 * @param $url link to build from 527 * @return built link 528 */ 529 public function buildLink($url) 530 { 531 $i = strpos($url, '://'); 532 if ($i !== FALSE || substr($url, 0, 1) == '#') return $url; 533 534 return wl($url); 535 } 536 537 /* 538 * Call syntax renderer of mikio syntax plugin 539 * 540 * @param $renderer DokuWiki renderer object 541 * @param $className mikio syntax class to call 542 * @param $text unmatched text to pass outside of lexer. Only used when $lexer=MIKIO_LEXER_AUTO 543 * @param $data tag options to pass to syntax class. Runs through cleanOptions to validate first 544 * @param $lexer which lexer to call 545 */ 546 public function syntaxRender(Doku_Renderer $renderer, $className, $text, $data = null, $lexer = MIKIO_LEXER_AUTO) 547 { 548 $className = 'syntax_plugin_mikioplugin_'.$className; 549 550 if(class_exists($className)) { 551 $class = new $className; 552 553 if(!is_array($data)) $data = array(); 554 555 556 if(count($class->options) > 0) { 557 $data = $class->cleanOptions($data, $class->options); 558 } 559 560 switch($lexer) { 561 case MIKIO_LEXER_AUTO: 562 if ($class->hasEndTag) { 563 if(method_exists($class, 'render_lexer_enter')) $class->render_lexer_enter($renderer, $data); 564 $renderer->doc .= $text; 565 if(method_exists($class, 'render_lexer_exit')) $class->render_lexer_exit($renderer, $data); 566 } else { 567 if(method_exists($class, 'render_lexer_special')) $class->render_lexer_special($renderer, $data); 568 } 569 570 break; 571 case MIKIO_LEXER_ENTER: 572 if(method_exists($class, 'render_lexer_enter')) $class->render_lexer_enter($renderer, $data); 573 break; 574 case MIKIO_LEXER_EXIT: 575 if(method_exists($class, 'render_lexer_exit')) $class->render_lexer_exit($renderer, $data); 576 break; 577 case MIKIO_LEXER_SPECIAL: 578 if(method_exists($class, 'render_lexer_special')) $class->render_lexer_special($renderer, $data); 579 break; 580 } 581 } 582 } 583 584 585 protected function callMikioTag($className, $data) { 586 // $className = 'syntax_plugin_mikioplugin_'.$className; 587 588 589 // if(class_exists($className)) { 590 //$class = new $className; 591 if(!plugin_isdisabled('mikioplugin')) { 592 $class = plugin_load('syntax', 'mikioplugin_' . $className); 593 // echo '^^'.$className.'^^'; 594 595 596 if(method_exists($class, 'mikioCall')) return $class->mikioCall($data); 597 598 } 599 600 // } 601 602 return ''; 603 } 604 605 606 protected function callMikioOptionDefault($className, $option) { 607 $className = 'syntax_plugin_mikioplugin_'.$className; 608 609 if(class_exists($className)) { 610 $class = new $className; 611 612 if(array_key_exists($option, $class->options) && array_key_exists('default', $class->options[$option])) { 613 return $class->options[$option]['default']; 614 } 615 } 616 617 return ''; 618 } 619 620 621 protected function buildTooltip($text) { 622 if($text != '') { 623 return ' data-tooltip="' . $text . '"'; 624 } 625 626 return ''; 627 } 628 629 /* 630 * Create array with passed elements and include them if their values are not empty 631 * 632 * @param ... array items 633 */ 634 protected function arrayRemoveEmpties($items) { 635 $result = array(); 636 637 foreach($items as $key => $value) { 638 if($value != '') { 639 $result[$key] = $value; 640 } 641 } 642 643 return $result; 644 } 645 646 public function getFirstArrayKey($data) 647 { 648 if (!function_exists('array_key_first')) { 649 foreach ($data as $key => $unused) { 650 return $key; 651 } 652 } 653 654 return array_key_first($data); 655 } 656 657 658 /* 659 * add common options to options 660 * 661 * @param $typelist common option to add 662 * @param $options save in options 663 */ 664 public function addCommonOptions($typelist) { 665 $types = explode(' ', $typelist); 666 foreach($types as $type) { 667 if(strcasecmp($type, 'shadow') == 0) { 668 $this->options['shadow'] = array('type' => 'choice', 669 'data' => array('large'=> array('shadow-large', 'shadow-lg'), 'small' => array('shadow-small', 'shadow-sm'), true), 670 'default' => '', 671 'class' => true); 672 } 673 674 if(strcasecmp($type, 'width') == 0) { 675 $this->options['width'] = array('type' => 'size', 676 'default' => ''); 677 } 678 679 if(strcasecmp($type, 'height') == 0) { 680 $this->options['height'] = array('type' => 'size', 681 'default' => ''); 682 } 683 684 if(strcasecmp($type, 'text-color') == 0) { 685 $this->options['text-color'] = array('type' => 'color', 686 'default' => ''); 687 } 688 689 if(strcasecmp($type, 'type') == 0) { 690 $this->options['type'] = array('type' => 'text', 691 'data' => array('primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'), 692 'default' => '', 693 'class' => true); 694 } 695 696 if(strcasecmp($type, 'text-align') == 0) { 697 $this->options['text-align'] = array('type' => 'choice', 698 'data' => array('left' => array('text-left'), 'center' => array('text-center'), 'right' => array('text-right')), 699 'default' => '', 700 'class' => true); 701 } 702 703 if(strcasecmp($type, 'align') == 0) { 704 $this->options['align'] = array('type' => 'choice', 705 'data' => array('left' => array('align-left'), 'center' => array('align-center'), 'right' => array('align-right')), 706 'default' => '', 707 'class' => true); 708 } 709 710 if(strcasecmp($type, 'tooltip') == 0) { 711 $this->options['tooltip'] = array('type' => 'text', 712 'default' => '', 713 'class' => true, 714 'classNoSuffix' => true); 715 } 716 717 if(strcasecmp($type, 'vertical-align') == 0) { 718 $this->options['vertical-align'] = array('type' => 'choice', 719 'data' => array('top' => array('align-top'), 'middle' => array('align-middle'), 'bottom' => array('align-bottom')), 720 'default' => '', 721 'class' => true); 722 } 723 724 if(strcasecmp($type, 'links-match') == 0) { 725 $this->options['links-match'] = array('type' => 'boolean', 726 'default' => 'false', 727 'class' => true); 728 } 729 730 } 731 } 732 733 734 /* 735 * Find HTML tags in string. Parse tags options. Used in parsing subtags 736 * 737 * @param $tagName tagName to search for. Name is exclusive 738 * @param $content search within content 739 * @param $options parse options similar to syntax element options 740 * @param $hasEndTag tagName search also looks for an end tag 741 * @return array of tags containing 'options' => array of 'name' => 'value', 'content' => content inside the tag 742 */ 743 protected function findTags($tagName, $content, $options, $hasEndTag = true) { 744 $items = array(); 745 $search = '/<(?i:' . $tagName . ')(.*?)>(.*?)<\/(?i:' . $tagName . ')>/s'; 746 747 if(!$hasEndTag) { 748 $search = '/<(?i:' . $tagName . ')(.*?)>/s'; 749 } 750 751 if(preg_match_all($search, $content, $match)) { 752 if(count($match) >= 2) { 753 for($i = 0; $i < count($match[1]); $i++) { 754 $item = array('options' => array(), 'content' => $this->render_text($match[2][$i])); 755 756 $optionlist = preg_split('/\s(?=([^"]*"[^"]*")*[^"]*$)/', trim($match[1][$i])); 757 758 foreach ($optionlist as $option) { 759 $j = strpos($option, '='); 760 if ($j !== false) { 761 $value = substr($option, $j + 1); 762 763 if (substr($value, 0, 1) == '"') $value = substr($value, 1); 764 if (substr($value, -1) == '"') $value = substr($value, 0, -1); 765 766 $item['options'][substr($option, 0, $j)] = $value; 767 } else { 768 $item['options'][$option] = true; 769 } 770 } 771 772 $item['options'] = $this->cleanOptions($item['options'], $options); 773 774 $items[] = $item; 775 } 776 } 777 } 778 779 return $items; 780 } 781}