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