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