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