xref: /plugin/combo/ComboStrap/PluginUtility.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
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];
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            $value = $COMBO[$name];
504        }
505        if ($value == null) {
506            return $default;
507        } else {
508            return $value;
509        }
510
511    }
512
513    /**
514     * Create an URL to the documentation website
515     * @param $canonical - canonical id or slug
516     * @param $label -  the text of the link
517     * @param bool $withIcon - used to break the recursion with the message in the {@link IconDownloader}
518     * @return string - an url
519     */
520    public
521    static function getDocumentationHyperLink($canonical, $label, bool $withIcon = true, $tooltip = ""): string
522    {
523
524        $xhtmlIcon = "";
525        if ($withIcon) {
526
527            $logoPath = WikiPath::createComboResource("images:logo.svg");
528            try {
529                $fetchImage = FetcherSvg::createSvgFromPath($logoPath);
530                $fetchImage->setRequestedType(FetcherSvg::ICON_TYPE)
531                    ->setRequestedWidth(20);
532                $xhtmlIcon = SvgImageLink::createFromFetcher($fetchImage)
533                    ->renderMediaTag();
534            } catch (ExceptionCompile $e) {
535                /**
536                 * We don't throw because this function
537                 * is also used by:
538                 *   * the log functionality to show link to the documentation creating a loop
539                 *   * inside the configuration description crashing the page
540                 */
541                if (PluginUtility::isDevOrTest()) {
542// shows errors in the html only on dev/test
543                    $xhtmlIcon = "Error: {$e->getMessage()}";
544                }
545            }
546
547        }
548        $urlApex = self::$URL_APEX;
549        $path = str_replace(":", "/", $canonical);
550        if (empty($tooltip)) {
551            $title = $label;
552        } else {
553            $title = $tooltip;
554        }
555        $htmlToolTip = "";
556        if (!empty($tooltip)) {
557            $dataAttributeNamespace = Bootstrap::getDataNamespace();
558            $htmlToolTip = "data{$dataAttributeNamespace}-toggle=\"tooltip\"";
559        }
560        return "$xhtmlIcon<a href=\"$urlApex/$path\" title=\"$title\" $htmlToolTip style=\"text-decoration:none;\">$label</a>";
561    }
562
563    /**
564     * An utility function to not search every time which array should be first
565     * @param array $inlineAttributes - the component inline attributes
566     * @param array $defaultAttributes - the default configuration attributes
567     * @return array - a merged array
568     */
569    public
570    static function mergeAttributes(array $inlineAttributes, array $defaultAttributes = array())
571    {
572        return array_merge($defaultAttributes, $inlineAttributes);
573    }
574
575    /**
576     * A pattern for a container tag
577     * that needs to catch the content
578     *
579     * Use as a special pattern (substition)
580     *
581     * The {@link \syntax_plugin_combo_math} use it
582     * @param $tag
583     * @return string - a pattern
584     */
585    public
586    static function getLeafContainerTagPattern($tag)
587    {
588        return '<' . $tag . '.*?>.*?<\/' . $tag . '>';
589    }
590
591    /**
592     * Return the content of a tag
593     *
594     * <math>Content</math>
595     * @param $match
596     * @return string the content
597     */
598    public
599    static function getTagContent($match)
600    {
601// From the first >
602        $start = strpos($match, ">");
603        if ($start == false) {
604            LogUtility::msg("The match does not contain any opening tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
605            return "";
606        }
607        $match = substr($match, $start + 1);
608// If this is the last character, we get a false
609        if ($match == false) {
610            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
611            return "";
612        }
613
614        $end = strrpos($match, "</");
615        if ($end == false) {
616            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
617            return "";
618        }
619
620        return substr($match, 0, $end);
621    }
622
623    /**
624     *
625     * Check if a HTML tag was already added for a request
626     * The request id is just the timestamp
627     * An indicator array should be provided
628     * @return string
629     */
630    public
631    static function getRequestId()
632    {
633
634        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
635// since php 5.4
636            $requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
637        } else {
638// DokuWiki test framework use this
639            $requestTime = $_SERVER['REQUEST_TIME'];
640        }
641        $keyPrefix = 'combo_';
642
643        global $ID;
644        return $keyPrefix . hash('crc32b', $_SERVER['REMOTE_ADDR'] . $_SERVER['REMOTE_PORT'] . $requestTime . $ID);
645
646    }
647
648    /**
649     *
650     * Return the requested wiki id (known also as page id)
651     *
652     * If the code is rendering a sidebar, it will not return the id of the sidebar
653     * but the requested wiki id
654     *
655     * @return string
656     * @throws ExceptionNotFound
657     * @deprecated use {@link ExecutionContext::getRequestedPath()}
658     */
659    public static function getRequestedWikiId(): string
660    {
661
662        return ExecutionContext::getActualOrCreateFromEnv()->getRequestedPath()->getWikiId();
663
664    }
665
666    public static function xmlEncode($text)
667    {
668        /**
669         * {@link htmlentities }
670         */
671        return htmlentities($text, ENT_XML1);
672    }
673
674
675    /**
676     * Add a class
677     * @param $classValue
678     * @param array $attributes
679     */
680    public
681    static function addClass2Attributes($classValue, array &$attributes)
682    {
683        self::addAttributeValue("class", $classValue, $attributes);
684    }
685
686    /**
687     * Add a style property to the attributes
688     * @param $property
689     * @param $value
690     * @param array $attributes
691     * @deprecated use {@link TagAttributes::addStyleDeclarationIfNotSet()} instead
692     */
693    public
694    static function addStyleProperty($property, $value, array &$attributes)
695    {
696        if (isset($attributes["style"])) {
697            $attributes["style"] .= ";$property:$value";
698        } else {
699            $attributes["style"] = "$property:$value";
700        }
701
702    }
703
704    /**
705     * Add default border attributes
706     * to see a border
707     * Doc
708     * https://combostrap.com/styling/color#border_color
709     * @param TagAttributes $tagAttributes
710     */
711    private
712    static function checkDefaultBorderColorAttributes(&$tagAttributes)
713    {
714        /**
715         * border color was set without the width
716         * setting the width
717         */
718        if (!(
719            $tagAttributes->hasStyleDeclaration("border")
720            ||
721            $tagAttributes->hasStyleDeclaration("border-width")
722        )
723        ) {
724            $tagAttributes->addStyleDeclarationIfNotSet("border-width", "1px");
725        }
726        /**
727         * border color was set without the style
728         * setting the style
729         */
730        if (!
731        (
732            $tagAttributes->hasStyleDeclaration("border")
733            ||
734            $tagAttributes->hasStyleDeclaration("border-style")
735        )
736        ) {
737            $tagAttributes->addStyleDeclarationIfNotSet("border-style", "solid");
738
739        }
740        if (!$tagAttributes->hasStyleDeclaration("border-radius")) {
741            $tagAttributes->addStyleDeclarationIfNotSet("border-radius", ".25rem");
742        }
743
744    }
745
746    /**
747     * @param $match
748     * @return null|string - return the tag name or null if not found
749     */
750    public
751    static function getMarkupTag($match): ?string
752    {
753
754        // Until the first >
755        $pos = strpos($match, ">");
756        if (!$pos) {
757            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
758            return null;
759        }
760        $match = substr($match, 0, $pos);
761
762        // if this is a empty tag with / at the end we delete it
763        if ($match[strlen($match) - 1] == "/") {
764            $match = substr($match, 0, -1);
765        }
766
767        // Suppress the <
768        if ($match[0] == "<") {
769            $match = substr($match, 1);
770            // closing tag
771            if ($match[0] == "/") {
772                $match = substr($match, 1);
773            }
774        } else {
775            LogUtility::msg("This is not a text tag because it does not start with the character `>`");
776        }
777
778        // Suppress the tag name (ie until the first blank)
779        $spacePosition = strpos($match, " ");
780        if (!$spacePosition) {
781            // No space, meaning this is only the tag name
782            return $match;
783        } else {
784            return substr($match, 0, $spacePosition);
785        }
786
787    }
788
789
790    public
791    static function getComponentName($tag): string
792    {
793        return strtolower(PluginUtility::PLUGIN_BASE_NAME) . "_" . $tag;
794    }
795
796    public
797    static function addAttributeValue($attribute, $value, array &$attributes)
798    {
799        if (array_key_exists($attribute, $attributes) && $attributes[$attribute] !== "") {
800            $attributes[$attribute] .= " {$value}";
801        } else {
802            $attributes[$attribute] = "{$value}";
803        }
804    }
805
806    /**
807     * Plugin Utility is available to all plugin,
808     * this is a convenient way to the the snippet manager
809     * @return SnippetSystem
810     */
811    public
812    static function getSnippetManager(): SnippetSystem
813    {
814        return SnippetSystem::getFromContext();
815    }
816
817
818    /**
819     * Function used in a render
820     * @param $data - the data from {@link PluginUtility::handleAndReturnUnmatchedData()}
821     * @return string
822     *
823     *
824     */
825    public
826    static function renderUnmatched($data): string
827    {
828        /**
829         * Attributes
830         */
831        $attributes = $data[PluginUtility::ATTRIBUTES] ?? [];
832        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
833
834        /**
835         * Display
836         */
837        $display = $tagAttributes->getValueAndRemoveIfPresent(Display::DISPLAY);
838        if ($display === "none") {
839            return "";
840        }
841
842        $payload = $data[self::PAYLOAD];
843        $previousTagDisplayType = $data[self::CONTEXT];
844        if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
845            // Delete the eol at the beginning and end
846            // otherwise we get a big block
847            $payload = ltrim($payload);
848        }
849        return Html::encode($payload);
850
851    }
852
853    public
854    static function renderUnmatchedXml($data)
855    {
856        $payload = $data[self::PAYLOAD];
857        $previousTagDisplayType = $data[self::CONTEXT];
858        if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
859            $payload = ltrim($payload);
860        }
861        return PluginUtility::xmlEncode($payload);
862
863    }
864
865    /**
866     * Function used in a handle function of a syntax plugin for
867     * unmatched context
868     * @param $tagName
869     * @param $match
870     * @param \Doku_Handler $handler
871     * @return array
872     */
873    public
874    static function handleAndReturnUnmatchedData($tagName, $match, \Doku_Handler $handler): array
875    {
876        $callStack = CallStack::createFromHandler($handler);
877        $sibling = $callStack->previous();
878        $context = null;
879        if (!empty($sibling)) {
880            $context = $sibling->getDisplay();
881        }
882        return array(
883            PluginUtility::STATE => DOKU_LEXER_UNMATCHED,
884            PluginUtility::PAYLOAD => $match,
885            PluginUtility::CONTEXT => $context
886        );
887    }
888
889    /**
890     * Utility methodPreprocess a start tag to be able to extract the name
891     * and the attributes easily
892     *
893     * It will delete:
894     *   * the characters <> and the /> if present
895     *   * and trim
896     *
897     * It will remain the tagname and its attributes
898     * @param $match
899     * @return false|string|null
900     */
901    private
902    static function getPreprocessEnterTag($match)
903    {
904// Until the first >
905        $pos = strpos($match, ">");
906        if (!$pos) {
907            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_WARNING);
908            return null;
909        }
910        $match = substr($match, 0, $pos);
911
912
913// Trim to start clean
914        $match = trim($match);
915
916// Suppress the <
917        if ($match[0] == "<") {
918            $match = substr($match, 1);
919        }
920
921// Suppress the / for a leaf tag
922        if ($match[strlen($match) - 1] == "/") {
923            $match = substr($match, 0, strlen($match) - 1);
924        }
925        return $match;
926    }
927
928    /**
929     * Retrieve the tag name used in the text document
930     * @param $match
931     * @return false|string|null
932     */
933    public
934    static function getSyntaxTagNameFromMatch($match)
935    {
936        $preprocessMatch = PluginUtility::getPreprocessEnterTag($match);
937
938// Tag name (ie until the first blank)
939        $spacePosition = strpos($match, " ");
940        if (!$spacePosition) {
941// No space, meaning this is only the tag name
942            return $preprocessMatch;
943        } else {
944            return trim(substr(0, $spacePosition));
945        }
946
947    }
948
949    /**
950     * Add an enter call to the stack
951     * @param \Doku_Handler $handler
952     * @param $tagName
953     * @param array $callStackArray
954     */
955    public
956    static function addEnterCall(
957        \Doku_Handler &$handler,
958                      $tagName,
959                      $callStackArray = array()
960    )
961    {
962        $pluginName = PluginUtility::getComponentName($tagName);
963        $handler->addPluginCall(
964            $pluginName,
965            $callStackArray,
966            DOKU_LEXER_ENTER,
967            null,
968            null
969        );
970    }
971
972    /**
973     * Add an end call dynamically
974     * @param \Doku_Handler $handler
975     * @param $tagName
976     * @param array $callStackArray
977     */
978    public
979    static function addEndCall(\Doku_Handler $handler, $tagName, $callStackArray = array())
980    {
981        $pluginName = PluginUtility::getComponentName($tagName);
982        $handler->addPluginCall(
983            $pluginName,
984            $callStackArray,
985            DOKU_LEXER_EXIT,
986            null,
987            null
988        );
989    }
990
991    /**
992     * General Debug
993     */
994    public
995    static function isDebug()
996    {
997        global $conf;
998        return $conf["allowdebug"] === 1;
999
1000    }
1001
1002
1003    /**
1004     *
1005     * See also dev.md file
1006     */
1007    public static function isDevOrTest()
1008    {
1009        if (self::isDev()) {
1010            return true;
1011        }
1012        return self::isTest();
1013    }
1014
1015    /**
1016     * Is this a dev environment (ie laptop where the dev is working)
1017     * @return bool
1018     */
1019    public static function isDev(): bool
1020    {
1021        global $_SERVER;
1022        if ($_SERVER["REMOTE_ADDR"] == "127.0.0.1") {
1023            return true;
1024        }
1025        if ($_SERVER["COMPUTERNAME"] === "NICO") {
1026            return true;
1027        }
1028        return false;
1029    }
1030
1031    public static function getInstructions($markiCode)
1032    {
1033        return p_get_instructions($markiCode);
1034    }
1035
1036    public static function getInstructionsWithoutRoot($markiCode)
1037    {
1038        return MarkupRenderUtility::getInstructionsAndStripPEventually($markiCode);
1039    }
1040
1041    public static function isTest(): bool
1042    {
1043        return defined('DOKU_UNITTEST');
1044    }
1045
1046
1047    public static function getCacheManager(): CacheManager
1048    {
1049        return CacheManager::getFromContextExecution();
1050    }
1051
1052    public static function getModeFromPluginName($name)
1053    {
1054        return "plugin_$name";
1055    }
1056
1057    public static function isCi(): bool
1058    {
1059        // https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
1060        // https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables
1061        return getenv("CI") === "true";
1062    }
1063
1064
1065    /**
1066     * @throws ExceptionCompile
1067     */
1068    public static function renderInstructionsToXhtml($callStackHeaderInstructions): ?string
1069    {
1070        return MarkupRenderUtility::renderInstructionsToXhtml($callStackHeaderInstructions);
1071    }
1072
1073    /**
1074     * @deprecated for {@link ExecutionContext::getExecutingWikiId()}
1075     */
1076    public static function getCurrentSlotId(): string
1077    {
1078        return ExecutionContext::getActualOrCreateFromEnv()->getExecutingWikiId();
1079    }
1080
1081
1082}
1083
1084PluginUtility::init();
1085