xref: /plugin/combo/ComboStrap/PluginUtility.php (revision 977ce05d19d8dab0a70c9a27f8da0b7039299e82)
1<?php
2
3
4namespace ComboStrap;
5
6
7use dokuwiki\Extension\Plugin;
8use dokuwiki\Extension\SyntaxPlugin;
9use PHPUnit\Exception;
10
11require_once(__DIR__ . '/../vendor/autoload.php');
12
13/**
14 * Parent in th hierarchy should be first
15 * Ie before {@link ImageLink, SvgImageLink, RasterImageLink)
16 */
17require_once(__DIR__ . '/CachedDocument.php');
18require_once(__DIR__ . '/PageCompilerDocument.php');
19require_once(__DIR__ . '/OutputDocument.php');
20require_once(__DIR__ . '/FileSystem.php');
21require_once(__DIR__ . '/Path.php');
22require_once(__DIR__ . '/PathAbs.php');
23require_once(__DIR__ . '/File.php');
24require_once(__DIR__ . '/DokuFs.php');
25require_once(__DIR__ . '/DokuPath.php');
26require_once(__DIR__ . '/ResourceCombo.php');
27require_once(__DIR__ . '/ResourceComboAbs.php');
28require_once(__DIR__ . '/Media.php');
29require_once(__DIR__ . '/MediaLink.php');
30require_once(__DIR__ . '/Metadata.php');
31require_once(__DIR__ . '/MetadataBoolean.php');
32require_once(__DIR__ . '/MetadataDateTime.php');
33require_once(__DIR__ . '/MetadataMultiple.php');
34require_once(__DIR__ . '/MetadataTabular.php');
35require_once(__DIR__ . '/MetadataText.php');
36require_once(__DIR__ . '/MetadataJson.php');
37require_once(__DIR__ . '/MetadataWikiPath.php');
38require_once(__DIR__ . '/MetadataStore.php');
39require_once(__DIR__ . '/MetadataStoreAbs.php');
40require_once(__DIR__ . '/MetadataSingleArrayStore.php');
41require_once(__DIR__ . '/XmlDocument.php');
42
43/**
44 * Plugin Utility is added in all Dokuwiki extension
45 * and
46 * all classes are added in plugin utility
47 *
48 * This is an utility master and the class loader
49 *
50 * If the load is relative, the load path is used
51 * and the bad php file may be loaded
52 * Furthermore, the absolute path helps
53 * the IDE when refactoring
54 */
55require_once(__DIR__ . '/AdsUtility.php');
56require_once(__DIR__ . '/Alias.php');
57require_once(__DIR__ . '/AliasPath.php');
58require_once(__DIR__ . '/AliasType.php');
59require_once(__DIR__ . '/Aliases.php');
60require_once(__DIR__ . '/Align.php');
61require_once(__DIR__ . '/AnalyticsDocument.php');
62require_once(__DIR__ . '/AnalyticsMenuItem.php');
63require_once(__DIR__ . '/Animation.php');
64require_once(__DIR__ . '/ArrayCaseInsensitive.php');
65require_once(__DIR__ . '/ArrayUtility.php');
66require_once(__DIR__ . '/Background.php');
67require_once(__DIR__ . '/BacklinkCount.php');
68require_once(__DIR__ . '/BacklinkMenuItem.php');
69require_once(__DIR__ . '/Boldness.php');
70require_once(__DIR__ . '/Boolean.php');
71require_once(__DIR__ . '/Bootstrap.php');
72require_once(__DIR__ . '/Brand.php');
73require_once(__DIR__ . '/BrandColors.php');
74require_once(__DIR__ . '/BrandButton.php');
75require_once(__DIR__ . '/CacheDependencies.php');
76require_once(__DIR__ . '/CacheExpirationDate.php');
77require_once(__DIR__ . '/CacheExpirationFrequency.php');
78require_once(__DIR__ . '/CacheLog.php');
79require_once(__DIR__ . '/CacheManager.php');
80require_once(__DIR__ . '/CacheMedia.php');
81require_once(__DIR__ . '/CacheMenuItem.php');
82require_once(__DIR__ . '/CacheReportHtmlDataBlockArray.php');
83require_once(__DIR__ . '/CacheResults.php');
84require_once(__DIR__ . '/CacheResult.php');
85require_once(__DIR__ . '/Call.php');
86require_once(__DIR__ . '/CallStack.php');
87require_once(__DIR__ . '/Canonical.php');
88require_once(__DIR__ . '/ColorRgb.php');
89require_once(__DIR__ . '/ColorHsl.php');
90require_once(__DIR__ . '/ComboStrap.php');
91require_once(__DIR__ . '/ConditionalValue.php');
92require_once(__DIR__ . '/Console.php');
93require_once(__DIR__ . '/Cron.php');
94require_once(__DIR__ . '/DatabasePageRow.php');
95require_once(__DIR__ . '/DataType.php');
96require_once(__DIR__ . '/Dictionary.php');
97require_once(__DIR__ . '/Dimension.php');
98require_once(__DIR__ . '/DisqusIdentifier.php');
99require_once(__DIR__ . '/Display.php');
100require_once(__DIR__ . '/DokuwikiUrl.php');
101require_once(__DIR__ . '/DokuwikiId.php');
102require_once(__DIR__ . '/EndDate.php');
103require_once(__DIR__ . '/Event.php');
104require_once(__DIR__ . '/ExitException.php');
105require_once(__DIR__ . '/ExceptionCombo.php');
106require_once(__DIR__ . '/ExceptionComboNotFound.php');
107require_once(__DIR__ . '/ExceptionComboRuntime.php');
108require_once(__DIR__ . '/FileSystems.php');
109require_once(__DIR__ . '/FloatAttribute.php');
110require_once(__DIR__ . '/FormMeta.php');
111require_once(__DIR__ . '/FormMetaTab.php');
112require_once(__DIR__ . '/FormMetaField.php');
113require_once(__DIR__ . '/FontSize.php');
114require_once(__DIR__ . '/FsWikiUtility.php');
115require_once(__DIR__ . '/HeaderUtility.php');
116require_once(__DIR__ . '/HtmlDocument.php');
117require_once(__DIR__ . '/HistoricalBreadcrumbMenuItem.php');
118require_once(__DIR__ . '/Hover.php');
119require_once(__DIR__ . '/Html.php');
120require_once(__DIR__ . '/Http.php');
121require_once(__DIR__ . '/HttpResponse.php');
122require_once(__DIR__ . '/Identity.php');
123require_once(__DIR__ . '/Image.php');
124require_once(__DIR__ . '/ImageLink.php');
125require_once(__DIR__ . '/ImageRaster.php');
126require_once(__DIR__ . '/ImageSvg.php');
127require_once(__DIR__ . '/Icon.php'); // icon is an image svg and should be after
128require_once(__DIR__ . '/Index.php');
129require_once(__DIR__ . '/InstructionsDocument.php');
130require_once(__DIR__ . '/InternetPath.php');
131require_once(__DIR__ . '/InterWikiPath.php');
132require_once(__DIR__ . '/Iso8601Date.php');
133require_once(__DIR__ . '/Json.php');
134require_once(__DIR__ . '/JavascriptLibrary.php');
135require_once(__DIR__ . '/Lang.php');
136require_once(__DIR__ . '/LdJson.php');
137require_once(__DIR__ . '/LineSpacing.php');
138require_once(__DIR__ . '/Locale.php');
139require_once(__DIR__ . '/LocalFs.php');
140require_once(__DIR__ . '/LocalPath.php');
141require_once(__DIR__ . '/LogException.php');
142require_once(__DIR__ . '/LogUtility.php');
143require_once(__DIR__ . '/LowQualityPage.php');
144require_once(__DIR__ . '/LowQualityPageOverwrite.php');
145require_once(__DIR__ . '/LowQualityCalculatedIndicator.php');
146require_once(__DIR__ . '/MarkupRef.php');
147require_once(__DIR__ . '/Math.php');
148require_once(__DIR__ . '/MetaManagerForm.php');
149require_once(__DIR__ . '/MetaManagerMenuItem.php');
150require_once(__DIR__ . '/MetadataDokuWikiStore.php');
151require_once(__DIR__ . '/MetadataFormDataStore.php');
152require_once(__DIR__ . '/MetadataFrontmatterStore.php');
153require_once(__DIR__ . '/MetadataDbStore.php');
154require_once(__DIR__ . '/MetadataStoreTransfer.php');
155require_once(__DIR__ . '/Message.php');
156require_once(__DIR__ . '/Mermaid.php');
157require_once(__DIR__ . '/Mime.php');
158require_once(__DIR__ . '/ModificationDate.php');
159require_once(__DIR__ . '/NavBarUtility.php');
160require_once(__DIR__ . '/Opacity.php');
161require_once(__DIR__ . '/Os.php');
162require_once(__DIR__ . '/Page.php');
163require_once(__DIR__ . '/PageDescription.php');
164require_once(__DIR__ . '/PageEdit.php');
165require_once(__DIR__ . '/PageId.php');
166require_once(__DIR__ . '/PageKeywords.php');
167require_once(__DIR__ . '/PageImages.php');
168require_once(__DIR__ . '/PageImage.php');
169require_once(__DIR__ . '/PageImagePath.php');
170require_once(__DIR__ . '/PageImageUsage.php');
171require_once(__DIR__ . '/PageLayout.php');
172require_once(__DIR__ . '/PagePath.php');
173require_once(__DIR__ . '/PageProtection.php');
174require_once(__DIR__ . '/PageRules.php');
175require_once(__DIR__ . '/PageSql.php');
176require_once(__DIR__ . '/PageSqlParser/PageSqlLexer.php');
177require_once(__DIR__ . '/PageSqlParser/PageSqlParser.php');
178require_once(__DIR__ . '/PageSqlTreeListener.php');
179require_once(__DIR__ . '/PageType.php');
180require_once(__DIR__ . '/PageTitle.php');
181require_once(__DIR__ . '/PageUrlPath.php');
182require_once(__DIR__ . '/PageUrlType.php');
183require_once(__DIR__ . '/PipelineUtility.php');
184require_once(__DIR__ . '/Position.php');
185require_once(__DIR__ . '/Prism.php');
186require_once(__DIR__ . '/PagePublicationDate.php');
187require_once(__DIR__ . '/PageCreationDate.php');
188require_once(__DIR__ . '/PageH1.php');
189require_once(__DIR__ . '/QualityDynamicMonitoringOverwrite.php');
190require_once(__DIR__ . '/QualityMenuItem.php');
191require_once(__DIR__ . '/RasterImageLink.php');
192require_once(__DIR__ . '/Region.php');
193require_once(__DIR__ . '/RenderUtility.php');
194require_once(__DIR__ . '/ReplicationDate.php');
195require_once(__DIR__ . '/ResourceName.php');
196require_once(__DIR__ . '/Sanitizer.php');
197require_once(__DIR__ . '/Shadow.php');
198require_once(__DIR__ . '/Site.php');
199require_once(__DIR__ . '/Skin.php');
200require_once(__DIR__ . '/Slug.php');
201require_once(__DIR__ . '/Snippet.php');
202require_once(__DIR__ . '/SnippetManager.php');
203require_once(__DIR__ . '/Spacing.php');
204require_once(__DIR__ . '/Sqlite.php');
205require_once(__DIR__ . '/SqliteRequest.php');
206require_once(__DIR__ . '/SqliteResult.php');
207require_once(__DIR__ . '/StringUtility.php');
208require_once(__DIR__ . '/StartDate.php');
209require_once(__DIR__ . '/StyleUtility.php');
210require_once(__DIR__ . '/SvgDocument.php');
211require_once(__DIR__ . '/SvgImageLink.php');
212require_once(__DIR__ . '/Syntax.php');
213require_once(__DIR__ . '/TableUtility.php');
214require_once(__DIR__ . '/Tag.php');
215require_once(__DIR__ . '/TagAttributes.php');
216require_once(__DIR__ . '/Template.php');
217require_once(__DIR__ . '/TemplateStore.php');
218require_once(__DIR__ . '/TemplateUtility.php');
219require_once(__DIR__ . '/TextAlign.php');
220require_once(__DIR__ . '/TextColor.php');
221require_once(__DIR__ . '/ThirdMedia.php');
222require_once(__DIR__ . '/ThirdMediaLink.php');
223require_once(__DIR__ . '/ThirdPartyPlugins.php');
224require_once(__DIR__ . '/TocUtility.php');
225require_once(__DIR__ . '/Toggle.php');
226require_once(__DIR__ . '/Tooltip.php');
227require_once(__DIR__ . '/References.php');
228require_once(__DIR__ . '/Reference.php');
229require_once(__DIR__ . '/Underline.php');
230require_once(__DIR__ . '/Unit.php');
231require_once(__DIR__ . '/Url.php');
232require_once(__DIR__ . '/UrlManagerBestEndPage.php');
233require_once(__DIR__ . '/XhtmlUtility.php');
234require_once(__DIR__ . '/XmlDocument.php');
235require_once(__DIR__ . '/XmlUtility.php');
236
237
238/**
239 * Class url static
240 * List of static utilities
241 */
242class PluginUtility
243{
244
245    const DOKU_DATA_DIR = '/dokudata/pages';
246    const DOKU_CACHE_DIR = '/dokudata/cache';
247
248    /**
249     * Key in the data array between the handle and render function
250     */
251    const STATE = "state";
252    const PAYLOAD = "payload"; // The html or text
253    const ATTRIBUTES = "attributes";
254    // The context is generally the parent tag but it may be also the grandfather.
255    // It permits to determine the HTML that is outputted
256    const CONTEXT = 'context';
257    const TAG = "tag";
258
259    /**
260     * The name of the hidden/private namespace
261     * where the icon and other artifactory are stored
262     */
263    const COMBOSTRAP_NAMESPACE_NAME = "combostrap";
264
265    const PARENT = "parent";
266    const POSITION = "position";
267
268    /**
269     * Class to center an element
270     */
271    const CENTER_CLASS = "mx-auto";
272
273
274    const EDIT_SECTION_TARGET = 'section';
275    const EXIT_MESSAGE = "errorAtt";
276    const EXIT_CODE = "exit_code";
277    const DISPLAY = "display";
278
279    /**
280     * The URL base of the documentation
281     */
282    static $URL_APEX;
283
284
285    /**
286     * @var string - the plugin base name (ie the directory)
287     * ie $INFO_PLUGIN['base'];
288     * This is a constant because it permits code analytics
289     * such as verification of a path
290     */
291    const PLUGIN_BASE_NAME = "combo";
292
293    /**
294     * The name of the template plugin
295     */
296    const TEMPLATE_STRAP_NAME = "strap";
297
298    /**
299     * @var array
300     */
301    static $INFO_PLUGIN;
302
303    static $PLUGIN_LANG;
304
305    /**
306     * The plugin name
307     * (not the same than the base as it's not related to the directory
308     * @var string
309     */
310    public static $PLUGIN_NAME;
311    /**
312     * @var mixed the version
313     */
314    private static $VERSION;
315
316
317    /**
318     * Initiate the static variable
319     * See the call after this class
320     */
321    static function init()
322    {
323
324        $pluginInfoFile = __DIR__ . '/../plugin.info.txt';
325        self::$INFO_PLUGIN = confToHash($pluginInfoFile);
326        self::$PLUGIN_NAME = 'ComboStrap';
327        global $lang;
328        self::$PLUGIN_LANG = $lang[self::PLUGIN_BASE_NAME];
329        self::$URL_APEX = "https://" . parse_url(self::$INFO_PLUGIN['url'], PHP_URL_HOST);
330        self::$VERSION = self::$INFO_PLUGIN['version'];
331
332    }
333
334    /**
335     * @param $inputExpression
336     * @return false|int 1|0
337     * returns:
338     *    - 1 if the input expression is a pattern,
339     *    - 0 if not,
340     *    - FALSE if an error occurred.
341     */
342    static function isRegularExpression($inputExpression)
343    {
344
345        $regularExpressionPattern = "/(\\/.*\\/[gmixXsuUAJ]?)/";
346        return preg_match($regularExpressionPattern, $inputExpression);
347
348    }
349
350    /**
351     * Return a mode from a tag (ie from a {@link Plugin::getPluginComponent()}
352     * @param $tag
353     * @return string
354     *
355     * A mode is just a name for a class
356     * Example: $Parser->addMode('listblock',new Doku_Parser_Mode_ListBlock());
357     */
358    public static function getModeFromTag($tag)
359    {
360        return "plugin_" . self::getComponentName($tag);
361    }
362
363    /**
364     * @param $tag
365     * @return string
366     *
367     * Create a lookahead pattern for a container tag used to enter in a mode
368     */
369    public static function getContainerTagPattern($tag)
370    {
371        // this pattern ensure that the tag
372        // `accordion` will not intercept also the tag `accordionitem`
373        // where:
374        // ?: means non capturing group (to not capture the last >)
375        // (\s.*?): is a capturing group that starts with a space
376        $pattern = "(?:\s.*?>|>)";
377        return '<' . $tag . $pattern . '(?=.*?<\/' . $tag . '>)';
378    }
379
380    /**
381     * This pattern allows space after the tag name
382     * for an end tag
383     * As XHTML (https://www.w3.org/TR/REC-xml/#dt-etag)
384     * @param $tag
385     * @return string
386     */
387    public static function getEndTagPattern($tag)
388    {
389        return "</$tag\s*>";
390    }
391
392    /**
393     * @param $tag
394     * @return string
395     *
396     * Create a open tag pattern without lookahead.
397     * Used for
398     * @link https://dev.w3.org/html5/html-author/#void-elements-0
399     */
400    public static function getVoidElementTagPattern($tag)
401    {
402        return '<' . $tag . '.*?>';
403    }
404
405
406    /**
407     * Take an array  where the key is the attribute name
408     * and return a HTML tag string
409     *
410     * The attribute name and value are escaped
411     *
412     * @param $attributes - combo attributes
413     * @return string
414     * @deprecated to allowed background and other metadata, use {@link TagAttributes::toHtmlEnterTag()}
415     */
416    public static function array2HTMLAttributesAsString($attributes)
417    {
418
419        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
420        return $tagAttributes->toHTMLAttributeString();
421
422    }
423
424    /**
425     *
426     * Parse the attributes part of a match
427     *
428     * Example:
429     *   line-numbers="value"
430     *   line-numbers='value'
431     *
432     * This value may be in:
433     *   * configuration value
434     *   * as well as in the match of a {@link SyntaxPlugin}
435     *
436     * @param $string
437     * @return array
438     *
439     * To parse a match, use {@link PluginUtility::getTagAttributes()}
440     *
441     *
442     */
443    public static function parseAttributes($string)
444    {
445
446        $parameters = array();
447
448        // Rules
449        //  * name may be alone (ie true boolean attribute)
450        //  * a name may get a `-`
451        //  * there may be space every everywhere when the value is enclosed with a quote
452        //  * there may be no space in the value and between the equal sign when the value is not enclosed
453        //
454        // /i not case sensitive
455        $attributePattern = '\s*([-\w]+)\s*(?:=(\s*[\'"]([^`"]*)[\'"]\s*|[^\s]*))?';
456        $result = preg_match_all('/' . $attributePattern . '/i', $string, $matches);
457        if ($result != 0) {
458            foreach ($matches[1] as $key => $parameterKey) {
459
460                // group 3 (ie the value between quotes)
461                $value = $matches[3][$key];
462                if ($value == "") {
463                    // check the value without quotes
464                    $value = $matches[2][$key];
465                }
466                // if there is no value, this is a boolean
467                if ($value == "") {
468                    $value = true;
469                } else {
470                    $value = hsc($value);
471                }
472                $parameters[hsc(strtolower($parameterKey))] = $value;
473            }
474        }
475        return $parameters;
476
477    }
478
479    public static function getTagAttributes($match, $knownTypes = null): array
480    {
481        return self::getQualifiedTagAttributes($match, false, "", $knownTypes);
482    }
483
484    /**
485     * Return the attribute of a tag
486     * Because they are users input, they are all escaped
487     * @param $match
488     * @param $hasThirdValue - if true, the third parameter is treated as value, not a property and returned in the `third` key
489     * use for the code/file/console where they accept a name as third value
490     * @param $keyThirdArgument - if a third argument is found, return it with this key
491     * @param array|null $knownTypes
492     * @return array
493     */
494    public static function getQualifiedTagAttributes($match, $hasThirdValue, $keyThirdArgument, array $knownTypes = null): array
495    {
496
497        $match = PluginUtility::getPreprocessEnterTag($match);
498
499        // Suppress the tag name (ie until the first blank)
500        $spacePosition = strpos($match, " ");
501        if (!$spacePosition) {
502            // No space, meaning this is only the tag name
503            return array();
504        }
505        $match = trim(substr($match, $spacePosition));
506        if ($match == "") {
507            return array();
508        }
509
510        // Do we have a type as first argument ?
511        $attributes = array();
512        $spacePosition = strpos($match, " ");
513        if ($spacePosition) {
514            $nextArgument = substr($match, 0, $spacePosition);
515        } else {
516            $nextArgument = $match;
517        }
518
519        $isType = !strpos($nextArgument, "=");
520        if ($knownTypes !== null) {
521            if (!in_array($nextArgument, $knownTypes)) {
522                $isType = false;
523            }
524        }
525        if ($isType) {
526
527            $attributes["type"] = $nextArgument;
528            // Suppress the type
529            $match = substr($match, strlen($nextArgument));
530            $match = trim($match);
531
532            // Do we have a value as first argument ?
533            if (!empty($hasThirdValue)) {
534                $spacePosition = strpos($match, " ");
535                if ($spacePosition) {
536                    $nextArgument = substr($match, 0, $spacePosition);
537                } else {
538                    $nextArgument = $match;
539                }
540                if (!strpos($nextArgument, "=") && !empty($nextArgument)) {
541                    $attributes[$keyThirdArgument] = $nextArgument;
542                    // Suppress the third argument
543                    $match = substr($match, strlen($nextArgument));
544                    $match = trim($match);
545                }
546            }
547        }
548
549        // Parse the remaining attributes
550        $parsedAttributes = self::parseAttributes($match);
551
552        // Merge
553        $attributes = array_merge($attributes, $parsedAttributes);;
554
555        return $attributes;
556
557    }
558
559    /**
560     * @param array $styleProperties - an array of CSS properties with key, value
561     * @return string - the value for the style attribute (ie all rules where joined with the comma)
562     */
563    public static function array2InlineStyle(array $styleProperties)
564    {
565        $inlineCss = "";
566        foreach ($styleProperties as $key => $value) {
567            $inlineCss .= "$key:$value;";
568        }
569        // Suppress the last ;
570        if ($inlineCss[strlen($inlineCss) - 1] == ";") {
571            $inlineCss = substr($inlineCss, 0, -1);
572        }
573        return $inlineCss;
574    }
575
576    /**
577     * @param $tag
578     * @return string
579     * Create a pattern used where the tag is not a container.
580     * ie
581     * <br/>
582     * <icon/>
583     * This is generally used with a subtition plugin
584     * and a {@link Lexer::addSpecialPattern} state
585     * where the tag is just replaced
586     */
587    public static function getEmptyTagPattern($tag): string
588    {
589
590        /**
591         * A tag should start with the tag
592         * `(?=[/ ]{1})` - a space or the / (lookahead) => to allow allow tag name with minus character
593         * `(?![^/]>)` - it's not a normal tag (ie a > with the previous character that is not /)
594         * `[^>]*` then until the > is found (dokuwiki capture greedy, don't use the point character)
595         * then until the close `/>` character
596         */
597        return '<' . $tag . '(?=[/ ]{1})(?![^/]>)[^>]*\/>';
598    }
599
600    /**
601     * Just call this function from a class like that
602     *     getTageName(get_called_class())
603     * to get the tag name (ie the component plugin)
604     * of a syntax plugin
605     *
606     * @param $get_called_class
607     * @return string
608     */
609    public static function getTagName($get_called_class)
610    {
611        list(/* $t */, /* $p */, /* $n */, $c) = explode('_', $get_called_class, 4);
612        return (isset($c) ? $c : '');
613    }
614
615    /**
616     * Just call this function from a class like that
617     *     getAdminPageName(get_called_class())
618     * to get the page name of a admin plugin
619     *
620     * @param $get_called_class
621     * @return string - the admin page name
622     */
623    public static function getAdminPageName($get_called_class)
624    {
625        $names = explode('_', $get_called_class);
626        $names = array_slice($names, -2);
627        return implode('_', $names);
628    }
629
630    public static function getNameSpace()
631    {
632        // No : at the begin of the namespace please
633        return self::PLUGIN_BASE_NAME . ':';
634    }
635
636    /**
637     * @param $get_called_class - the plugin class
638     * @return array
639     */
640    public static function getTags($get_called_class)
641    {
642        $elements = array();
643        $elementName = PluginUtility::getTagName($get_called_class);
644        $elements[] = $elementName;
645        $elements[] = strtoupper($elementName);
646        return $elements;
647    }
648
649    /**
650     * Render a text
651     * @param $pageContent
652     * @return string|null
653     */
654    public static function render($pageContent)
655    {
656        return RenderUtility::renderText2XhtmlAndStripPEventually($pageContent, false);
657    }
658
659
660    /**
661     * This method will takes attributes
662     * and process the plugin styling attribute such as width and height
663     * to put them in a style HTML attribute
664     * @param TagAttributes $attributes
665     */
666    public static function processStyle(&$attributes)
667    {
668        // Style
669        $styleAttributeName = "style";
670        if ($attributes->hasComponentAttribute($styleAttributeName)) {
671            $properties = explode(";", $attributes->getValueAndRemove($styleAttributeName));
672            foreach ($properties as $property) {
673                list($key, $value) = explode(":", $property);
674                if ($key != "") {
675                    $attributes->addStyleDeclarationIfNotSet($key, $value);
676                }
677            }
678        }
679
680
681        /**
682         * Border Color
683         * For background color, see {@link TagAttributes::processBackground()}
684         * For text color, see {@link TextColor}
685         */
686
687        if ($attributes->hasComponentAttribute(ColorRgb::BORDER_COLOR)) {
688            $colorValue = $attributes->getValueAndRemove(ColorRgb::BORDER_COLOR);
689            $attributes->addStyleDeclarationIfNotSet(ColorRgb::BORDER_COLOR, ColorRgb::createFromString($colorValue)->toCssValue());
690            self::checkDefaultBorderColorAttributes($attributes);
691        }
692
693
694    }
695
696    /**
697     * Return the name of the requested script
698     */
699    public
700    static function getRequestScript()
701    {
702        $scriptPath = null;
703        $testPropertyValue = self::getPropertyValue("SCRIPT_NAME");
704        if (defined('DOKU_UNITTEST') && $testPropertyValue != null) {
705            return $testPropertyValue;
706        }
707        if (array_key_exists("DOCUMENT_URI", $_SERVER)) {
708            $scriptPath = $_SERVER["DOCUMENT_URI"];
709        }
710        if ($scriptPath == null && array_key_exists("SCRIPT_NAME", $_SERVER)) {
711            $scriptPath = $_SERVER["SCRIPT_NAME"];
712        }
713        if ($scriptPath == null) {
714            msg("Unable to find the main script", LogUtility::LVL_MSG_ERROR);
715        }
716        $path_parts = pathinfo($scriptPath);
717        return $path_parts['basename'];
718    }
719
720    /**
721     *
722     * @param $name
723     * @param $default
724     * @return string - the value of a query string property or if in test mode, the value of a test variable
725     * set with {@link self::setTestProperty}
726     * This is used to test script that are not supported by the dokuwiki test framework
727     * such as css.php
728     */
729    public
730    static function getPropertyValue($name, $default = null)
731    {
732        global $INPUT;
733        $value = $INPUT->str($name);
734        if ($value == null && defined('DOKU_UNITTEST')) {
735            global $COMBO;
736            $value = $COMBO[$name];
737        }
738        if ($value == null) {
739            return $default;
740        } else {
741            return $value;
742        }
743
744    }
745
746    /**
747     * Create an URL to the documentation website
748     * @param $canonical - canonical id or slug
749     * @param $label -  the text of the link
750     * @param bool $withIcon - used to break the recursion with the message in the {@link Icon}
751     * @return string - an url
752     */
753    public
754    static function getDocumentationHyperLink($canonical, $label, $withIcon = true, $tooltip = ""): string
755    {
756        /** @noinspection SpellCheckingInspection */
757
758        $xhtmlIcon = "";
759        if ($withIcon) {
760
761            /**
762             * We don't include it as an external resource via url
763             * because it then make a http request for every logo
764             * in the configuration page and makes it really slow
765             * TODO: when we have made a special fetch ajax with cache
766             * for application resource, we can serve it statically
767             */
768            $path = Site::getComboImagesDirectory()->resolve("logo.svg");
769            try {
770                $tagAttributes = TagAttributes::createEmpty(SvgImageLink::CANONICAL);
771                $tagAttributes->addComponentAttributeValue(TagAttributes::TYPE_KEY, SvgDocument::ICON_TYPE);
772                $tagAttributes->addComponentAttributeValue(Dimension::WIDTH_KEY, "20");
773                $cache = new CacheMedia($path, $tagAttributes);
774                if (!$cache->isCacheUsable()) {
775                    $xhtmlIcon = SvgDocument::createSvgDocumentFromPath($path)
776                        ->setShouldBeOptimized(true)
777                        ->getXmlText($tagAttributes);
778                    $cache->storeCache($xhtmlIcon);
779                }
780                $xhtmlIcon = FileSystems::getContent($cache->getFile());
781            } catch (ExceptionCombo $e) {
782                LogUtility::msg("The logo ($path) is not valid and could not be added to the documentation link. Error: {$e->getMessage()}");
783            }
784
785        }
786        $urlApex = self::$URL_APEX;
787        $path = str_replace(":", "/", $canonical);
788        if (empty($tooltip)) {
789            $title = $label;
790        } else {
791            $title = $tooltip;
792        }
793        $htmlToolTip = "";
794        if (!empty($tooltip)) {
795            $dataAttributeNamespace = Bootstrap::getDataNamespace();
796            $htmlToolTip = "data{$dataAttributeNamespace}-toggle=\"tooltip\"";
797        }
798        return "$xhtmlIcon<a href=\"$urlApex/$path\" title=\"$title\" $htmlToolTip style=\"text-decoration:none;\">$label</a>";
799    }
800
801    /**
802     * An utility function to not search every time which array should be first
803     * @param array $inlineAttributes - the component inline attributes
804     * @param array $defaultAttributes - the default configuration attributes
805     * @return array - a merged array
806     */
807    public
808    static function mergeAttributes(array $inlineAttributes, array $defaultAttributes = array())
809    {
810        return array_merge($defaultAttributes, $inlineAttributes);
811    }
812
813    /**
814     * A pattern for a container tag
815     * that needs to catch the content
816     *
817     * Use as a special pattern (substition)
818     *
819     * The {@link \syntax_plugin_combo_math} use it
820     * @param $tag
821     * @return string - a pattern
822     */
823    public
824    static function getLeafContainerTagPattern($tag)
825    {
826        return '<' . $tag . '.*?>.*?<\/' . $tag . '>';
827    }
828
829    /**
830     * Return the content of a tag
831     * <math>Content</math>
832     * @param $match
833     * @return string the content
834     */
835    public
836    static function getTagContent($match)
837    {
838        // From the first >
839        $start = strpos($match, ">");
840        if ($start == false) {
841            LogUtility::msg("The match does not contain any opening tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
842            return "";
843        }
844        $match = substr($match, $start + 1);
845        // If this is the last character, we get a false
846        if ($match == false) {
847            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
848            return "";
849        }
850
851        $end = strrpos($match, "</");
852        if ($end == false) {
853            LogUtility::msg("The match does not contain any closing tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
854            return "";
855        }
856
857        return substr($match, 0, $end);
858    }
859
860    /**
861     *
862     * Check if a HTML tag was already added for a request
863     * The request id is just the timestamp
864     * An indicator array should be provided
865     * @return string
866     */
867    public
868    static function getRequestId()
869    {
870
871        if (isset($_SERVER['REQUEST_TIME_FLOAT'])) {
872            // since php 5.4
873            $requestTime = $_SERVER['REQUEST_TIME_FLOAT'];
874        } else {
875            // DokuWiki test framework use this
876            $requestTime = $_SERVER['REQUEST_TIME'];
877        }
878        $keyPrefix = 'combo_';
879
880        global $ID;
881        return $keyPrefix . hash('crc32b', $_SERVER['REMOTE_ADDR'] . $_SERVER['REMOTE_PORT'] . $requestTime . $ID);
882
883    }
884
885    /**
886     * Get the page id
887     * If the page is a sidebar, it will not return the id of the sidebar
888     * but the one of the page
889     * Return the main/requested page id
890     * (Not the sidebar)
891     * @return string|null - null in test
892     */
893    public
894    static function getRequestedWikiId(): ?string
895    {
896        global $ID;
897        global $INFO;
898        $callingId = $ID;
899        // If the component is in a sidebar, we don't want the ID of the sidebar
900        // but the ID of the page.
901        if ($INFO !== null) {
902            $callingId = $INFO['id'];
903        }
904        /**
905         * This is the case with event triggered
906         * before DokuWiki such as
907         * https://www.dokuwiki.org/devel:event:init_lang_load
908         */
909        if ($callingId == null) {
910            global $_REQUEST;
911            if (isset($_REQUEST[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE])) {
912                $callingId = $_REQUEST[DokuwikiId::DOKUWIKI_ID_ATTRIBUTE];
913            }
914        }
915
916        return $callingId;
917
918    }
919
920    /**
921     * Encode special HTML characters to entity
922     * (ie escaping)
923     *
924     * This is used to transform text that may be interpreted as HTML
925     * into a text
926     *   * that will not be interpreted as HTML
927     *   * that may be added in html attribute
928     *
929     * For instance:
930     *  * text that should go in attribute with special HTML characters (such as title)
931     *  * text that we don't create (to prevent HTML injection)
932     *
933     * Example:
934     *
935     * <script>...</script>
936     * to
937     * "&lt;script&gt;...&lt;/hello&gt;"
938     *
939     *
940     * @param $text
941     * @return string
942     */
943    public
944    static function htmlEncode($text): string
945    {
946        /**
947         * See https://stackoverflow.com/questions/46483/htmlentities-vs-htmlspecialchars/3614344
948         *
949         * Not {@link htmlentities } htmlentities($text, ENT_QUOTES);
950         * Otherwise we get `Error while loading HTMLError: Entity 'hellip' not defined`
951         * when loading HTML with {@link XmlDocument}
952         *
953         * See also {@link PluginUtility::htmlDecode()}
954         *
955         * Without ENT_QUOTES
956         * <h4 class="heading-combo">
957         * is encoded as
958         * &gt;h4 class="heading-combo"&lt;
959         * and cannot be added in a attribute because of the quote
960         * This is used for {@link Tooltip}
961         */
962        return htmlspecialchars($text, ENT_XHTML | ENT_QUOTES);
963
964    }
965
966    public
967    static function xmlEncode($text)
968    {
969        /**
970         * {@link htmlentities }
971         */
972        return htmlentities($text, ENT_XML1);
973    }
974
975
976    /**
977     * Add a class
978     * @param $classValue
979     * @param array $attributes
980     */
981    public
982    static function addClass2Attributes($classValue, array &$attributes)
983    {
984        self::addAttributeValue("class", $classValue, $attributes);
985    }
986
987    /**
988     * Add a style property to the attributes
989     * @param $property
990     * @param $value
991     * @param array $attributes
992     * @deprecated use {@link TagAttributes::addStyleDeclarationIfNotSet()} instead
993     */
994    public
995    static function addStyleProperty($property, $value, array &$attributes)
996    {
997        if (isset($attributes["style"])) {
998            $attributes["style"] .= ";$property:$value";
999        } else {
1000            $attributes["style"] = "$property:$value";
1001        }
1002
1003    }
1004
1005    /**
1006     * Add default border attributes
1007     * to see a border
1008     * Doc
1009     * https://combostrap.com/styling/color#border_color
1010     * @param TagAttributes $tagAttributes
1011     */
1012    private
1013    static function checkDefaultBorderColorAttributes(&$tagAttributes)
1014    {
1015        /**
1016         * border color was set without the width
1017         * setting the width
1018         */
1019        if (!(
1020            $tagAttributes->hasStyleDeclaration("border")
1021            ||
1022            $tagAttributes->hasStyleDeclaration("border-width")
1023        )
1024        ) {
1025            $tagAttributes->addStyleDeclarationIfNotSet("border-width", "1px");
1026        }
1027        /**
1028         * border color was set without the style
1029         * setting the style
1030         */
1031        if (!
1032        (
1033            $tagAttributes->hasStyleDeclaration("border")
1034            ||
1035            $tagAttributes->hasStyleDeclaration("border-style")
1036        )
1037        ) {
1038            $tagAttributes->addStyleDeclarationIfNotSet("border-style", "solid");
1039
1040        }
1041        if (!$tagAttributes->hasStyleDeclaration("border-radius")) {
1042            $tagAttributes->addStyleDeclarationIfNotSet("border-radius", ".25rem");
1043        }
1044
1045    }
1046
1047    public
1048    static function getConfValue($confName, $defaultValue = null)
1049    {
1050        global $conf;
1051        $value = $conf['plugin'][PluginUtility::PLUGIN_BASE_NAME][$confName];
1052        if ($value === null || trim($value) === "") {
1053            return $defaultValue;
1054        }
1055        return $value;
1056    }
1057
1058    /**
1059     * @param $match
1060     * @return null|string - return the tag name or null if not found
1061     */
1062    public
1063    static function getTag($match)
1064    {
1065
1066        // Trim to start clean
1067        $match = trim($match);
1068
1069        // Until the first >
1070        $pos = strpos($match, ">");
1071        if ($pos == false) {
1072            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_ERROR);
1073            return null;
1074        }
1075        $match = substr($match, 0, $pos);
1076
1077        // Suppress the <
1078        if ($match[0] == "<") {
1079            $match = substr($match, 1);
1080        } else {
1081            LogUtility::msg("This is not a text tag because it does not start with the character `>`");
1082        }
1083
1084        // Suppress the tag name (ie until the first blank)
1085        $spacePosition = strpos($match, " ");
1086        if (!$spacePosition) {
1087            // No space, meaning this is only the tag name
1088            return $match;
1089        } else {
1090            return substr($match, 0, $spacePosition);
1091        }
1092
1093    }
1094
1095
1096    /**
1097     * @param string $string add a command into HTML
1098     */
1099    public
1100    static function addAsHtmlComment($string)
1101    {
1102        print_r('<!-- ' . self::htmlEncode($string) . '-->');
1103    }
1104
1105    public
1106    static function getResourceBaseUrl()
1107    {
1108        return DOKU_URL . 'lib/plugins/' . PluginUtility::PLUGIN_BASE_NAME . '/resources';
1109    }
1110
1111
1112    public
1113    static function getComponentName($tag)
1114    {
1115        return strtolower(PluginUtility::PLUGIN_BASE_NAME) . "_" . $tag;
1116    }
1117
1118    public
1119    static function addAttributeValue($attribute, $value, array &$attributes)
1120    {
1121        if (array_key_exists($attribute, $attributes) && $attributes[$attribute] !== "") {
1122            $attributes[$attribute] .= " {$value}";
1123        } else {
1124            $attributes[$attribute] = "{$value}";
1125        }
1126    }
1127
1128    /**
1129     * Plugin Utility is available to all plugin,
1130     * this is a convenient way to the the snippet manager
1131     * @return SnippetManager
1132     */
1133    public
1134    static function getSnippetManager(): SnippetManager
1135    {
1136        return SnippetManager::getOrCreate();
1137    }
1138
1139
1140    /**
1141     * Function used in a render
1142     * @param $data - the data from {@link PluginUtility::handleAndReturnUnmatchedData()}
1143     * @return string
1144     */
1145    public
1146    static function renderUnmatched($data): string
1147    {
1148        /**
1149         * Attributes
1150         */
1151        if (isset($data[PluginUtility::ATTRIBUTES])) {
1152            $attributes = $data[PluginUtility::ATTRIBUTES];
1153        } else {
1154            $attributes = [];
1155        }
1156        $tagAttributes = TagAttributes::createFromCallStackArray($attributes);
1157        $display = $tagAttributes->getValue(Display::DISPLAY);
1158        if ($display !== "none") {
1159            $payload = $data[self::PAYLOAD];
1160            $previousTagDisplayType = $data[self::CONTEXT];
1161            if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
1162                $payload = ltrim($payload);
1163            }
1164            return PluginUtility::htmlEncode($payload);
1165        } else {
1166            return "";
1167        }
1168    }
1169
1170    public
1171    static function renderUnmatchedXml($data)
1172    {
1173        $payload = $data[self::PAYLOAD];
1174        $previousTagDisplayType = $data[self::CONTEXT];
1175        if ($previousTagDisplayType !== Call::INLINE_DISPLAY) {
1176            $payload = ltrim($payload);
1177        }
1178        return PluginUtility::xmlEncode($payload);
1179
1180    }
1181
1182    /**
1183     * Function used in a handle function of a syntax plugin for
1184     * unmatched context
1185     * @param $tagName
1186     * @param $match
1187     * @param \Doku_Handler $handler
1188     * @return array
1189     */
1190    public
1191    static function handleAndReturnUnmatchedData($tagName, $match, \Doku_Handler $handler): array
1192    {
1193        $callStack = CallStack::createFromHandler($handler);
1194        $sibling = $callStack->previous();
1195        $context = null;
1196        if (!empty($sibling)) {
1197            $context = $sibling->getDisplay();
1198        }
1199        return array(
1200            PluginUtility::STATE => DOKU_LEXER_UNMATCHED,
1201            PluginUtility::PAYLOAD => $match,
1202            PluginUtility::CONTEXT => $context
1203        );
1204    }
1205
1206    public
1207    static function setConf($key, $value, $namespace = 'plugin')
1208    {
1209        global $conf;
1210        if ($namespace !== null) {
1211            $conf[$namespace][PluginUtility::PLUGIN_BASE_NAME][$key] = $value;
1212        } else {
1213            $conf[$key] = $value;
1214        }
1215
1216    }
1217
1218    /**
1219     * Utility methodPreprocess a start tag to be able to extract the name
1220     * and the attributes easily
1221     *
1222     * It will delete:
1223     *   * the characters <> and the /> if present
1224     *   * and trim
1225     *
1226     * It will remain the tagname and its attributes
1227     * @param $match
1228     * @return false|string|null
1229     */
1230    private
1231    static function getPreprocessEnterTag($match)
1232    {
1233        // Until the first >
1234        $pos = strpos($match, ">");
1235        if ($pos == false) {
1236            LogUtility::msg("The match does not contain any tag. Match: {$match}", LogUtility::LVL_MSG_WARNING);
1237            return null;
1238        }
1239        $match = substr($match, 0, $pos);
1240
1241
1242        // Trim to start clean
1243        $match = trim($match);
1244
1245        // Suppress the <
1246        if ($match[0] == "<") {
1247            $match = substr($match, 1);
1248        }
1249
1250        // Suppress the / for a leaf tag
1251        if ($match[strlen($match) - 1] == "/") {
1252            $match = substr($match, 0, strlen($match) - 1);
1253        }
1254        return $match;
1255    }
1256
1257    /**
1258     * Retrieve the tag name used in the text document
1259     * @param $match
1260     * @return false|string|null
1261     */
1262    public
1263    static function getSyntaxTagNameFromMatch($match)
1264    {
1265        $preprocessMatch = PluginUtility::getPreprocessEnterTag($match);
1266
1267        // Tag name (ie until the first blank)
1268        $spacePosition = strpos($match, " ");
1269        if (!$spacePosition) {
1270            // No space, meaning this is only the tag name
1271            return $preprocessMatch;
1272        } else {
1273            return trim(substr(0, $spacePosition));
1274        }
1275
1276    }
1277
1278    /**
1279     * @param \Doku_Renderer_xhtml $renderer
1280     * @param $position
1281     * @param $name
1282     */
1283    public
1284    static function startSection($renderer, $position, $name)
1285    {
1286
1287
1288        if (empty($position)) {
1289            LogUtility::msg("The position for a start section should not be empty", LogUtility::LVL_MSG_ERROR, "support");
1290        }
1291        if (empty($name)) {
1292            LogUtility::msg("The name for a start section should not be empty", LogUtility::LVL_MSG_ERROR, "support");
1293        }
1294
1295        /**
1296         * New Dokuwiki Version
1297         * for DokuWiki Greebo and more recent versions
1298         */
1299        if (defined('SEC_EDIT_PATTERN')) {
1300            $renderer->startSectionEdit($position, array('target' => self::EDIT_SECTION_TARGET, 'name' => $name));
1301        } else {
1302            /**
1303             * Old version
1304             */
1305            /** @noinspection PhpParamsInspection */
1306            $renderer->startSectionEdit($position, self::EDIT_SECTION_TARGET, $name);
1307        }
1308    }
1309
1310    /**
1311     * Add an enter call to the stack
1312     * @param \Doku_Handler $handler
1313     * @param $tagName
1314     * @param array $callStackArray
1315     */
1316    public
1317    static function addEnterCall(
1318        \Doku_Handler &$handler,
1319        $tagName,
1320        $callStackArray = array()
1321    )
1322    {
1323        $pluginName = PluginUtility::getComponentName($tagName);
1324        $handler->addPluginCall(
1325            $pluginName,
1326            $callStackArray,
1327            DOKU_LEXER_ENTER,
1328            null,
1329            null
1330        );
1331    }
1332
1333
1334    /**
1335     * General Debug
1336     */
1337    public
1338    static function isDebug()
1339    {
1340        global $conf;
1341        return $conf["allowdebug"] === 1;
1342
1343    }
1344
1345
1346    /**
1347     *
1348     * See also dev.md file
1349     */
1350    public static function isDevOrTest()
1351    {
1352        if (self::isDev()) {
1353            return true;
1354        }
1355        return self::isTest();
1356    }
1357
1358    /**
1359     * Is this a dev environment (ie laptop where the dev is working)
1360     * @return bool
1361     */
1362    public static function isDev(): bool
1363    {
1364        global $_SERVER;
1365        if ($_SERVER["REMOTE_ADDR"] == "127.0.0.1") {
1366            return true;
1367        }
1368        if ($_SERVER["COMPUTERNAME"] === "NICO") {
1369            return true;
1370        }
1371        return false;
1372    }
1373
1374    public static function getInstructions($markiCode)
1375    {
1376        return p_get_instructions($markiCode);
1377    }
1378
1379    public static function getInstructionsWithoutRoot($markiCode)
1380    {
1381        return RenderUtility::getInstructionsAndStripPEventually($markiCode);
1382    }
1383
1384    public static function isTest()
1385    {
1386        return defined('DOKU_UNITTEST');
1387    }
1388
1389
1390    public static function getCacheManager(): CacheManager
1391    {
1392        return CacheManager::getOrCreate();
1393    }
1394
1395    public static function getModeFromPluginName($name)
1396    {
1397        return "plugin_$name";
1398    }
1399
1400    public static function isCi(): bool
1401    {
1402        // https://docs.travis-ci.com/user/environment-variables/#default-environment-variables
1403        return getenv("CI") === "true";
1404    }
1405
1406    public static function htmlDecode($int): string
1407    {
1408        return htmlspecialchars_decode($int, ENT_XHTML | ENT_QUOTES);
1409    }
1410
1411    /**
1412     * Tells if the process is to output a page
1413     * @return bool
1414     */
1415    public static function isRenderingRequestedPageProcess(): bool
1416    {
1417
1418        global $ID;
1419        if (empty($ID)) {
1420            // $ID is null
1421            // case on "/lib/exe/mediamanager.php"
1422            return false;
1423        }
1424
1425        $page = Page::createPageFromId($ID);
1426        if (!$page->exists()) {
1427            return false;
1428        }
1429
1430        /**
1431         * No metadata for bars
1432         */
1433        if ($page->isSecondarySlot()) {
1434            return false;
1435        }
1436        return true;
1437
1438    }
1439
1440    /**
1441     * @throws ExceptionCombo
1442     */
1443    public static function renderInstructionsToXhtml($callStackHeaderInstructions): ?string
1444    {
1445        return RenderUtility::renderInstructionsToXhtml($callStackHeaderInstructions);
1446    }
1447
1448    /**
1449     */
1450    public static function getCurrentSlotId()
1451    {
1452        global $ID;
1453        $slot = $ID;
1454        if ($slot === null) {
1455            if (!PluginUtility::isTest()) {
1456                LogUtility::msg("The slot could not be identified (global ID is null)");
1457            }
1458            return RenderUtility::DEFAULT_SLOT_ID_FOR_TEST;
1459        }
1460        return $slot;
1461    }
1462
1463
1464}
1465
1466PluginUtility::init();
1467