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