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