1<?php
2
3
4namespace ComboStrap;
5
6
7use ComboStrap\Api\ApiRouter;
8use dokuwiki\Extension\Plugin;
9use dokuwiki\Extension\SyntaxPlugin;
10
11
12/**
13 * Class url static
14 * List of static utilities
15 */
16class PluginUtility
17{
18
19    const DOKU_DATA_DIR = '/dokudata/pages';
20    const DOKU_CACHE_DIR = '/dokudata/cache';
21
22    /**
23     * Key in the data array between the handle and render function
24     */
25    const STATE = "state";
26    const PAYLOAD = "payload"; // The html or text
27    const ATTRIBUTES = "attributes";
28// The context is generally the parent tag but it may be also the grandfather.
29// It permits to determine the HTML that is outputted
30    const CONTEXT = 'context';
31    const TAG = "tag";
32
33    /**
34     * The name of the hidden/private namespace
35     * where the icon and other artifactory are stored
36     */
37    const COMBOSTRAP_NAMESPACE_NAME = "combostrap";
38
39    const PARENT = "parent";
40    const POSITION = "position";
41
42
43    const EXIT_MESSAGE = "exit_message";
44    const EXIT_CODE = "exit_code";
45
46    const DISPLAY = "display";
47    const MARKUP_TAG = "markup-tag";
48
49
50    /**
51     * The URL base of the documentation
52     */
53    static $URL_APEX;
54
55
56    /**
57     * @var string - the plugin base name (ie the directory)
58     * ie $INFO_PLUGIN['base'];
59     * This is a constant because it permits code analytics
60     * such as verification of a path
61     */
62    const PLUGIN_BASE_NAME = "combo";
63
64    /**
65     * The name of the template plugin
66     */
67    const TEMPLATE_STRAP_NAME = "strap";
68
69    /**
70     * @var array
71     */
72    static $INFO_PLUGIN;
73
74    static $PLUGIN_LANG;
75
76    /**
77     * The plugin name
78     * (not the same than the base as it's not related to the directory
79     * @var string
80     */
81    public static $PLUGIN_NAME;
82    /**
83     * @var LocalPath
84     */
85    private static $PLUGIN_INFO_FILE;
86
87
88    /**
89     * Initiate the static variable
90     * See the call after this class
91     */
92    static function init()
93    {
94
95        $pluginInfoFile = DirectoryLayout::getPluginInfoPath();
96        self::$INFO_PLUGIN = confToHash($pluginInfoFile->toAbsoluteId());
97        self::$PLUGIN_NAME = 'ComboStrap';
98        global $lang;
99        self::$PLUGIN_LANG = $lang[self::PLUGIN_BASE_NAME] ?? null;
100        self::$URL_APEX = "https://" . parse_url(self::$INFO_PLUGIN['url'], PHP_URL_HOST);
101        //self::$VERSION = self::$INFO_PLUGIN['version'];
102
103    }
104
105    /**
106     * @param $inputExpression
107     * @return false|int 1|0
108     * returns:
109     *    - 1 if the input expression is a pattern,
110     *    - 0 if not,
111     *    - FALSE if an error occurred.
112     */
113    static function isRegularExpression($inputExpression)
114    {
115
116        $regularExpressionPattern = "/(\\/.*\\/[gmixXsuUAJ]?)/";
117        return preg_match($regularExpressionPattern, $inputExpression);
118
119    }
120
121    /**
122     * Return a mode from a tag (ie from a {@link Plugin::getPluginComponent()}
123     * @param $tag
124     * @return string
125     *
126     * A mode is just a name for a class
127     * Example: $Parser->addMode('listblock',new Doku_Parser_Mode_ListBlock());
128     */
129    public static function getModeFromTag($tag)
130    {
131        return "plugin_" . self::getComponentName($tag);
132    }
133
134
135    /**
136     * This pattern allows space after the tag name
137     * for an end tag
138     * As XHTML (https://www.w3.org/TR/REC-xml/#dt-etag)
139     * @param $tag
140     * @return string
141     */
142    public static function getEndTagPattern($tag)
143    {
144        return "</$tag\s*>";
145    }
146
147    /**
148     * @param $tag
149     * @return string
150     *
151     * Create a open tag pattern without lookahead.
152     * Used for
153     * @link https://dev.w3.org/html5/html-author/#void-elements-0
154     */
155    public static function getVoidElementTagPattern($tag)
156    {
157        return ' < ' . $tag . ' .*?>';
158    }
159
160
161    /**
162     * Take an array  where the key is the attribute name
163     * and return a HTML tag string
164     *
165     * The attribute name and value are escaped
166     *
167     * @param $attributes - combo attributes
168     * @return string
169     * @deprecated to allowed background and other metadata, use {@link TagAttributes::toHtmlEnterTag()}
170     */
171    public static function array2HTMLAttributesAsString($attributes)
172    {
173
174        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
175        return $tagAttributes->toHTMLAttributeString();
176
177    }
178
179    /**
180     *
181     * Parse the attributes part of a match
182     *
183     * Example:
184     *   line-numbers="value"
185     *   line-numbers='value'
186     *
187     * This value may be in:
188     *   * configuration value
189     *   * as well as in the match of a {@link SyntaxPlugin}
190     *
191     * @param $string
192     * @return array
193     *
194     * To parse a match, use {@link PluginUtility::getTagAttributes()}
195     *
196     *
197     */
198    public static function parseAttributes($string)
199    {
200
201        $parameters = array();
202
203// Rules
204//  * name may be alone (ie true boolean attribute)
205//  * a name may get a `-`
206//  * there may be space every everywhere when the value is enclosed with a quote
207//  * there may be no space in the value and between the equal sign when the value is not enclosed
208//
209// /i not case sensitive
210        $attributePattern = '\s*([-\w]+)\s*(?:=(\s*[\'"]([^`"]*)[\'"]\s*|[^\s]*))?';
211        $result = preg_match_all('/' . $attributePattern . '/i', $string, $matches);
212        if ($result != 0) {
213            foreach ($matches[1] as $key => $parameterKey) {
214
215// group 3 (ie the value between quotes)
216                $value = $matches[3][$key];
217                if ($value == "") {
218// check the value without quotes
219                    $value = $matches[2][$key];
220                }
221// if there is no value, this is a boolean
222                if ($value == "") {
223                    $value = true;
224                } else {
225                    $value = hsc($value);
226                }
227                $parameters[hsc(strtolower($parameterKey))] = $value;
228            }
229        }
230        return $parameters;
231
232    }
233
234    public static function getTagAttributes(string $match, array $knownTypes = [], bool $allowFirstBooleanAttributesAsType = false): array
235    {
236        return self::getQualifiedTagAttributes($match, false, "", $knownTypes, $allowFirstBooleanAttributesAsType);
237    }
238
239    /**
240     * Return the attribute of a tag
241     * Because they are users input, they are all escaped
242     * @param $match
243     * @param $hasThirdValue - if true, the third parameter is treated as value, not a property and returned in the `third` key
244     * use for the code/file/console where they accept a name as third value
245     * @param $keyThirdArgument - if a third argument is found, return it with this key
246     * @param array|null $knownTypes
247     * @param bool $allowFirstBooleanAttributesAsType
248     * @return array
249     */
250    public static function getQualifiedTagAttributes($match, $hasThirdValue, $keyThirdArgument, array $knownTypes = [], bool $allowFirstBooleanAttributesAsType = false): array
251    {
252
253        $match = PluginUtility::getPreprocessEnterTag($match);
254
255        // Suppress the tag name (ie until the first blank)
256        $spacePosition = strpos($match, " ");
257        if (!$spacePosition) {
258            // No space, meaning this is only the tag name
259            return array();
260        }
261        $match = trim(substr($match, $spacePosition));
262        if ($match == "") {
263            return array();
264        }
265
266        /**
267         * Do we have a type as first argument ?
268         */
269        $attributes = array();
270        $spacePosition = strpos($match, " ");
271        if ($spacePosition) {
272            $nextArgument = substr($match, 0, $spacePosition);
273        } else {
274            $nextArgument = $match;
275        }
276
277        $isBooleanAttribute = !strpos($nextArgument, "=");
278        $isType = false;
279        if ($isBooleanAttribute) {
280            $possibleTypeLowercase = strtolower($nextArgument);
281            if ($allowFirstBooleanAttributesAsType) {
282                $isType = true;
283                $nextArgument = $possibleTypeLowercase;
284            } else {
285                if (!empty($knownTypes) && in_array($possibleTypeLowercase, $knownTypes)) {
286                    $isType = true;
287                    $nextArgument = $possibleTypeLowercase;
288                }
289            }
290        }
291        if ($isType) {
292
293            $attributes[TagAttributes::TYPE_KEY] = $nextArgument;
294            /**
295             * Suppress the type
296             */
297            $match = substr($match, strlen($nextArgument));
298            $match = trim($match);
299
300            /**
301             * Do we have a value as first argument ?
302             */
303            if (!empty($hasThirdValue)) {
304                $spacePosition = strpos($match, " ");
305                if ($spacePosition) {
306                    $nextArgument = substr($match, 0, $spacePosition);
307                } else {
308                    $nextArgument = $match;
309                }
310                if (!strpos($nextArgument, "=") && !empty($nextArgument)) {
311                    $attributes[$keyThirdArgument] = $nextArgument;
312                    /**
313                     * Suppress the third argument
314                     */
315                    $match = substr($match, strlen($nextArgument));
316                    $match = trim($match);
317                }
318            }
319        }
320
321        /**
322         * Parse the remaining attributes
323         */
324        $parsedAttributes = self::parseAttributes($match);
325
326        /**
327         * Merge
328         */
329        $attributes = array_merge($attributes, $parsedAttributes);;
330
331        return $attributes;
332
333    }
334
335    /**
336     * @param $tag
337     * @return string
338     * Create a pattern used where the tag is not a container.
339     * ie
340     * <br/>
341     *
342     * <icon/>
343     * This is generally used with a subtition plugin
344     * and a {@link Lexer::addSpecialPattern} state
345     * where the tag is just replaced
346     */
347    public static function getEmptyTagPattern($tag): string
348    {
349
350        /**
351         * A tag should start with the tag
352         * `(?=[/ ]{1})` - a space or the / (lookahead) => to allow allow tag name with minus character
353         * `(?![^/]>)` - it's not a normal tag (ie a > with the previous character that is not /)
354         * `[^>]*` then until the > is found (dokuwiki capture greedy, don't use the point character)
355         * then until the close `/>` character
356         */
357        return '<' . $tag . '(?=[/ ]{1})(?![^/]>)[^>]*\/>';
358    }
359
360    public static function getEmptyTagPatternGeneral(): string
361    {
362
363        return self::getEmptyTagPattern("[\w-]+");
364    }
365
366    /**
367     * Just call this function from a class like that
368     *     getTageName(get_called_class())
369     * to get the tag name (ie the component plugin)
370     * of a syntax plugin
371     *
372     * @param $get_called_class
373     * @return string
374     */
375    public static function getTagName($get_called_class)
376    {
377        list(/* $t */, /* $p */, /* $n */, $c) = explode('_', $get_called_class, 4);
378        return (isset($c) ? $c : '');
379    }
380
381    /**
382     * Just call this function from a class like that
383     *     getAdminPageName(get_called_class())
384     * to get the page name of a admin plugin
385     *
386     * @param $get_called_class
387     * @return string - the admin page name
388     */
389    public static function getAdminPageName($get_called_class)
390    {
391        $names = explode('_', $get_called_class);
392        $names = array_slice($names, -2);
393        return implode('_', $names);
394    }
395
396    public static function getNameSpace()
397    {
398// No : at the begin of the namespace please
399        return self::PLUGIN_BASE_NAME . ':';
400    }
401
402    /**
403     * @param $get_called_class - the plugin class
404     * @return array
405     */
406    public static function getTags($get_called_class)
407    {
408        $elements = array();
409        $elementName = PluginUtility::getTagName($get_called_class);
410        $elements[] = $elementName;
411        $elements[] = strtoupper($elementName);
412        return $elements;
413    }
414
415    /**
416     * Render a text
417     * @param $pageContent
418     * @return string|null
419     */
420    public static function render($pageContent): ?string
421    {
422        return MarkupRenderUtility::renderText2XhtmlAndStripPEventually($pageContent, false);
423    }
424
425
426    /**
427     * This method will takes attributes
428     * and process the plugin styling attribute such as width and height
429     * to put them in a style HTML attribute
430     * @param TagAttributes $attributes
431     */
432    public static function processStyle(&$attributes)
433    {
434        // Style
435        $styleAttributeName = "style";
436        if ($attributes->hasComponentAttribute($styleAttributeName)) {
437            $properties = explode(";", $attributes->getValueAndRemove($styleAttributeName));
438            foreach ($properties as $property) {
439                list($key, $value) = explode(":", $property);
440                if ($key != "") {
441                    $attributes->addStyleDeclarationIfNotSet($key, $value);
442                }
443            }
444        }
445
446
447        /**
448         * Border Color
449         * For background color, see {@link TagAttributes::processBackground()}
450         * For text color, see {@link TextColor}
451         */
452
453        if ($attributes->hasComponentAttribute(ColorRgb::BORDER_COLOR)) {
454            $colorValue = $attributes->getValueAndRemove(ColorRgb::BORDER_COLOR);
455            $attributes->addStyleDeclarationIfNotSet(ColorRgb::BORDER_COLOR, ColorRgb::createFromString($colorValue)->toCssValue());
456            self::checkDefaultBorderColorAttributes($attributes);
457        }
458
459
460    }
461
462    /**
463     * Return the name of the requested script
464     */
465    public
466    static function getRequestScript()
467    {
468        $scriptPath = null;
469        $testPropertyValue = self::getPropertyValue("SCRIPT_NAME");
470        if (defined('DOKU_UNITTEST') && $testPropertyValue != null) {
471            return $testPropertyValue;
472        }
473        if (array_key_exists("DOCUMENT_URI", $_SERVER)) {
474            $scriptPath = $_SERVER["DOCUMENT_URI"];
475        }
476        if ($scriptPath == null && array_key_exists("SCRIPT_NAME", $_SERVER)) {
477            $scriptPath = $_SERVER["SCRIPT_NAME"];
478        }
479        if ($scriptPath == null) {
480            msg("Unable to find the main script", LogUtility::LVL_MSG_ERROR);
481        }
482        $path_parts = pathinfo($scriptPath);
483        return $path_parts['basename'];
484    }
485
486    /**
487     *
488     * @param $name
489     * @param $default
490     * @return string - the value of a query string property or if in test mode, the value of a test variable
491     * set with {@link self::setTestProperty}
492     * This is used to test script that are not supported by the dokuwiki test framework
493     * such as css.php
494     * @deprecated use {@link ApiRouter::getRequestParameter()}
495     */
496    public
497    static function getPropertyValue($name, $default = null)
498    {
499        global $INPUT;
500        $value = $INPUT->str($name);
501        if ($value == null && defined('DOKU_UNITTEST')) {
502            global $COMBO;
503            if ($COMBO !== null) {
504                $value = $COMBO[$name];
505            }
506        }
507        if ($value == null) {
508            return $default;
509        } else {
510            return $value;
511        }
512
513    }
514
515    /**
516     * Create an URL to the documentation website
517     * @param $canonical - canonical id or slug
518     * @param $label -  the text of the link
519     * @param bool $withIcon - used to break the recursion with the message in the {@link IconDownloader}
520     * @return string - an url
521     */
522    public
523    static function getDocumentationHyperLink($canonical, $label, bool $withIcon = true, $tooltip = ""): string
524    {
525
526        $xhtmlIcon = "";
527        if ($withIcon) {
528
529            $logoPath = WikiPath::createComboResource("images:logo.svg");
530            try {
531                $fetchImage = FetcherSvg::createSvgFromPath($logoPath);
532                $fetchImage->setRequestedType(FetcherSvg::ICON_TYPE)
533                    ->setRequestedWidth(20);
534                $xhtmlIcon = SvgImageLink::createFromFetcher($fetchImage)
535                    ->renderMediaTag();
536            } catch (ExceptionCompile $e) {
537                /**
538                 * We don't throw because this function
539                 * is also used by:
540                 *   * the log functionality to show link to the documentation creating a loop
541                 *   * inside the configuration description crashing the page
542                 */
543                if (PluginUtility::isDevOrTest()) {
544// shows errors in the html only on dev/test
545                    $xhtmlIcon = "Error: {$e->getMessage()}";
546                }
547            }
548
549        }
550        $urlApex = self::$URL_APEX;
551        $path = str_replace(":", "/", $canonical);
552        if (empty($tooltip)) {
553            $title = $label;
554        } else {
555            $title = $tooltip;
556        }
557        $htmlToolTip = "";
558        if (!empty($tooltip)) {
559            $dataAttributeNamespace = Bootstrap::getDataNamespace();
560            $htmlToolTip = "data{$dataAttributeNamespace}-toggle=\"tooltip\"";
561        }
562        return "$xhtmlIcon<a href=\"$urlApex/$path\" title=\"$title\" $htmlToolTip style=\"text-decoration:none;\">$label</a>";
563    }
564
565    /**
566     * An utility function to not search every time which array should be first
567     * @param array $inlineAttributes - the component inline attributes
568     * @param array $defaultAttributes - the default configuration attributes
569     * @return array - a merged array
570     */
571    public
572    static function mergeAttributes(array $inlineAttributes, array $defaultAttributes = array())
573    {
574        return array_merge($defaultAttributes, $inlineAttributes);
575    }
576
577    /**
578     * A pattern for a container tag
579     * that needs to catch the content
580     *
581     * Use as a special pattern (substition)
582     *
583     * The {@link \syntax_plugin_combo_math} use it
584     * @param $tag
585     * @return string - a pattern
586     */
587    public
588    static function getLeafContainerTagPattern($tag)
589    {
590        return '<' . $tag . '.*?>.*?<\/' . $tag . '>';
591    }
592
593    /**
594     * Return the content of a tag
595     *
596     * <math>Content</math>
597     * @param $match
598     * @return string the content
599     */
600    public
601    static function getTagContent($match)
602    {
603// From the first >
604        $start = strpos($match, ">");
605        if ($start == false) {
606            LogUtility::msg("The match does not contain any opening tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
607            return "";
608        }
609        $match = substr($match, $start + 1);
610// If this is the last character, we get a false
611        if ($match == false) {
612            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
613            return "";
614        }
615
616        $end = strrpos($match, "</");
617        if ($end == false) {
618            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
619            return "";
620        }
621
622        return substr($match, 0, $end);
623    }
624
625    /**
626     *
627     * Check if a HTML tag was already added for a request
628     * The request id is just the timestamp
629     * An indicator array should be provided
630     * @return string
631     */
632    public
633    static function getRequestId()
634    {
635
636        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
637// since php 5.4
638            $requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
639        } else {
640// DokuWiki test framework use this
641            $requestTime = $_SERVER['REQUEST_TIME'];
642        }
643        $keyPrefix = 'combo_';
644
645        global $ID;
646        return $keyPrefix . hash('crc32b', $_SERVER['REMOTE_ADDR'] . $_SERVER['REMOTE_PORT'] . $requestTime . $ID);
647
648    }
649
650    /**
651     *
652     * Return the requested wiki id (known also as page id)
653     *
654     * If the code is rendering a sidebar, it will not return the id of the sidebar
655     * but the requested wiki id
656     *
657     * @return string
658     * @throws ExceptionNotFound
659     * @deprecated use {@link ExecutionContext::getRequestedPath()}
660     */
661    public static function getRequestedWikiId(): string
662    {
663
664        return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath()->getWikiId();
665
666    }
667
668    public static function xmlEncode($text)
669    {
670        /**
671         * {@link htmlentities }
672         */
673        return htmlentities($text, ENT_XML1);
674    }
675
676
677    /**
678     * Add a class
679     * @param $classValue
680     * @param array $attributes
681     */
682    public
683    static function addClass2Attributes($classValue, array &$attributes)
684    {
685        self::addAttributeValue("class", $classValue, $attributes);
686    }
687
688    /**
689     * Add a style property to the attributes
690     * @param $property
691     * @param $value
692     * @param array $attributes
693     * @deprecated use {@link TagAttributes::addStyleDeclarationIfNotSet()} instead
694     */
695    public
696    static function addStyleProperty($property, $value, array &$attributes)
697    {
698        if (isset($attributes["style"])) {
699            $attributes["style"] .= ";$property:$value";
700        } else {
701            $attributes["style"] = "$property:$value";
702        }
703
704    }
705
706    /**
707     * Add default border attributes
708     * to see a border
709     * Doc
710     * https://combostrap.com/styling/color#border_color
711     * @param TagAttributes $tagAttributes
712     */
713    private
714    static function checkDefaultBorderColorAttributes(&$tagAttributes)
715    {
716        /**
717         * border color was set without the width
718         * setting the width
719         */
720        if (!(
721            $tagAttributes->hasStyleDeclaration("border")
722            ||
723            $tagAttributes->hasStyleDeclaration("border-width")
724        )
725        ) {
726            $tagAttributes->addStyleDeclarationIfNotSet("border-width", "1px");
727        }
728        /**
729         * border color was set without the style
730         * setting the style
731         */
732        if (!
733        (
734            $tagAttributes->hasStyleDeclaration("border")
735            ||
736            $tagAttributes->hasStyleDeclaration("border-style")
737        )
738        ) {
739            $tagAttributes->addStyleDeclarationIfNotSet("border-style", "solid");
740
741        }
742        if (!$tagAttributes->hasStyleDeclaration("border-radius")) {
743            $tagAttributes->addStyleDeclarationIfNotSet("border-radius", ".25rem");
744        }
745
746    }
747
748    /**
749     * @param $match
750     * @return null|string - return the tag name or null if not found
751     */
752    public
753    static function getMarkupTag($match): ?string
754    {
755
756        // Until the first >
757        $pos = strpos($match, ">");
758        if (!$pos) {
759            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
760            return null;
761        }
762        $match = substr($match, 0, $pos);
763
764        // if this is a empty tag with / at the end we delete it
765        if ($match[strlen($match) - 1] == "/") {
766            $match = substr($match, 0, -1);
767        }
768
769        // Suppress the <
770        if ($match[0] == "<") {
771            $match = substr($match, 1);
772            // closing tag
773            if ($match[0] == "/") {
774                $match = substr($match, 1);
775            }
776        } else {
777            LogUtility::msg("This is not a text tag because it does not start with the character `>`");
778        }
779
780        // Suppress the tag name (ie until the first blank)
781        $spacePosition = strpos($match, " ");
782        if (!$spacePosition) {
783            // No space, meaning this is only the tag name
784            return $match;
785        } else {
786            return substr($match, 0, $spacePosition);
787        }
788
789    }
790
791
792    public
793    static function getComponentName($tag): string
794    {
795        return strtolower(PluginUtility::PLUGIN_BASE_NAME) . "_" . $tag;
796    }
797
798    public
799    static function addAttributeValue($attribute, $value, array &$attributes)
800    {
801        if (array_key_exists($attribute, $attributes) && $attributes[$attribute] !== "") {
802            $attributes[$attribute] .= " {$value}";
803        } else {
804            $attributes[$attribute] = "{$value}";
805        }
806    }
807
808    /**
809     * Plugin Utility is available to all plugin,
810     * this is a convenient way to the the snippet manager
811     * @return SnippetSystem
812     */
813    public
814    static function getSnippetManager(): SnippetSystem
815    {
816        return SnippetSystem::getFromContext();
817    }
818
819
820    /**
821     * Function used in a render
822     * @param $data - the data from {@link PluginUtility::handleAndReturnUnmatchedData()}
823     * @return string
824     *
825     *
826     */
827    public
828    static function renderUnmatched($data): string
829    {
830        /**
831         * Attributes
832         */
833        $attributes = $data[PluginUtility::ATTRIBUTES] ?? [];
834        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
835
836        /**
837         * Display
838         */
839        $display = $tagAttributes->getValueAndRemoveIfPresent(Display::DISPLAY);
840        if ($display === "none") {
841            return "";
842        }
843
844        $payload = $data[self::PAYLOAD] ?? null;
845        $previousTagDisplayType = $data[self::CONTEXT] ?? null;
846        if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
847            // Delete the eol at the beginning and end
848            // otherwise we get a big block
849            $payload = ltrim($payload);
850        }
851        return Html::encode($payload);
852
853    }
854
855    public
856    static function renderUnmatchedXml($data)
857    {
858        $payload = $data[self::PAYLOAD];
859        $previousTagDisplayType = $data[self::CONTEXT];
860        if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
861            $payload = ltrim($payload);
862        }
863        return PluginUtility::xmlEncode($payload);
864
865    }
866
867    /**
868     * Function used in a handle function of a syntax plugin for
869     * unmatched context
870     * @param $tagName
871     * @param $match
872     * @param \Doku_Handler $handler
873     * @return array
874     */
875    public
876    static function handleAndReturnUnmatchedData($tagName, $match, \Doku_Handler $handler): array
877    {
878        $callStack = CallStack::createFromHandler($handler);
879        $sibling = $callStack->previous();
880        $context = null;
881        if (!empty($sibling)) {
882            $context = $sibling->getDisplay();
883        }
884        return array(
885            PluginUtility::STATE => DOKU_LEXER_UNMATCHED,
886            PluginUtility::PAYLOAD => $match,
887            PluginUtility::CONTEXT => $context
888        );
889    }
890
891    /**
892     * Utility methodPreprocess a start tag to be able to extract the name
893     * and the attributes easily
894     *
895     * It will delete:
896     *   * the characters <> and the /> if present
897     *   * and trim
898     *
899     * It will remain the tagname and its attributes
900     * @param $match
901     * @return false|string|null
902     */
903    private
904    static function getPreprocessEnterTag($match)
905    {
906// Until the first >
907        $pos = strpos($match, ">");
908        if (!$pos) {
909            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_WARNING);
910            return null;
911        }
912        $match = substr($match, 0, $pos);
913
914
915// Trim to start clean
916        $match = trim($match);
917
918// Suppress the <
919        if ($match[0] == "<") {
920            $match = substr($match, 1);
921        }
922
923// Suppress the / for a leaf tag
924        if ($match[strlen($match) - 1] == "/") {
925            $match = substr($match, 0, strlen($match) - 1);
926        }
927        return $match;
928    }
929
930    /**
931     * Retrieve the tag name used in the text document
932     * @param $match
933     * @return false|string|null
934     */
935    public
936    static function getSyntaxTagNameFromMatch($match)
937    {
938        $preprocessMatch = PluginUtility::getPreprocessEnterTag($match);
939
940// Tag name (ie until the first blank)
941        $spacePosition = strpos($match, " ");
942        if (!$spacePosition) {
943// No space, meaning this is only the tag name
944            return $preprocessMatch;
945        } else {
946            return trim(substr(0, $spacePosition));
947        }
948
949    }
950
951    /**
952     * Add an enter call to the stack
953     * @param \Doku_Handler $handler
954     * @param $tagName
955     * @param array $callStackArray
956     */
957    public
958    static function addEnterCall(
959        \Doku_Handler &$handler,
960                      $tagName,
961                      $callStackArray = array()
962    )
963    {
964        $pluginName = PluginUtility::getComponentName($tagName);
965        $handler->addPluginCall(
966            $pluginName,
967            $callStackArray,
968            DOKU_LEXER_ENTER,
969            null,
970            null
971        );
972    }
973
974    /**
975     * Add an end call dynamically
976     * @param \Doku_Handler $handler
977     * @param $tagName
978     * @param array $callStackArray
979     */
980    public
981    static function addEndCall(\Doku_Handler $handler, $tagName, $callStackArray = array())
982    {
983        $pluginName = PluginUtility::getComponentName($tagName);
984        $handler->addPluginCall(
985            $pluginName,
986            $callStackArray,
987            DOKU_LEXER_EXIT,
988            null,
989            null
990        );
991    }
992
993    /**
994     * General Debug
995     */
996    public
997    static function isDebug()
998    {
999        global $conf;
1000        return $conf["allowdebug"] === 1;
1001
1002    }
1003
1004
1005    /**
1006     *
1007     * See also dev.md file
1008     */
1009    public static function isDevOrTest()
1010    {
1011        if (self::isDev()) {
1012            return true;
1013        }
1014        return self::isTest();
1015    }
1016
1017    /**
1018     * Is this a dev environment (ie laptop where the dev is working)
1019     * @return bool
1020     */
1021    public static function isDev(): bool
1022    {
1023        global $_SERVER;
1024        $remoteAddr = $_SERVER["REMOTE_ADDR"] ?? null;
1025        if ($remoteAddr == "127.0.0.1") {
1026            return true;
1027        }
1028        $computerName = $_SERVER["COMPUTERNAME"] ?? null;
1029        if ($computerName === "NICO") {
1030            return true;
1031        }
1032        return false;
1033    }
1034
1035    public static function getInstructions($markiCode)
1036    {
1037        return p_get_instructions($markiCode);
1038    }
1039
1040    public static function getInstructionsWithoutRoot($markiCode)
1041    {
1042        return MarkupRenderUtility::getInstructionsAndStripPEventually($markiCode);
1043    }
1044
1045    public static function isTest(): bool
1046    {
1047        return defined('DOKU_UNITTEST');
1048    }
1049
1050
1051    public static function getCacheManager(): CacheManager
1052    {
1053        return CacheManager::getFromContextExecution();
1054    }
1055
1056    public static function getModeFromPluginName($name)
1057    {
1058        return "plugin_$name";
1059    }
1060
1061    public static function isCi(): bool
1062    {
1063        // https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
1064        // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
1065        return getenv("CI") === "true";
1066    }
1067
1068
1069    /**
1070     * @throws ExceptionCompile
1071     */
1072    public static function renderInstructionsToXhtml($callStackHeaderInstructions): ?string
1073    {
1074        return MarkupRenderUtility::renderInstructionsToXhtml($callStackHeaderInstructions);
1075    }
1076
1077    /**
1078     * @deprecated for {@link ExecutionContext::getExecutingWikiId()}
1079     */
1080    public static function getCurrentSlotId(): string
1081    {
1082        return ExecutionContext::getActualOrCreateFromEnv()->getExecutingWikiId();
1083    }
1084
1085
1086}
1087
1088PluginUtility::init();
1089