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