xref: /plugin/combo/syntax/link.php (revision ac4b853ad2aaf47a1ec7419ce1f054bf9b5d0976)
1007225e5Sgerardnico<?php
2007225e5Sgerardnico
3007225e5Sgerardnico
437748cd8SNickeaurequire_once(__DIR__ . "/../ComboStrap/PluginUtility.php");
5007225e5Sgerardnico
64cadd4f8SNickeauuse ComboStrap\ArrayUtility;
704fd306cSNickeauuse ComboStrap\ButtonTag;
84cadd4f8SNickeauuse ComboStrap\Call;
9531e725cSNickeauuse ComboStrap\CallStack;
1004fd306cSNickeauuse ComboStrap\DropDownTag;
1104fd306cSNickeauuse ComboStrap\ExceptionBadArgument;
1204fd306cSNickeauuse ComboStrap\ExceptionBadSyntax;
1304fd306cSNickeauuse ComboStrap\ExceptionCompile;
1404fd306cSNickeauuse ComboStrap\ExceptionNotFound;
1504fd306cSNickeauuse ComboStrap\FileSystems;
1604fd306cSNickeauuse ComboStrap\LinkMarkup;
174cadd4f8SNickeauuse ComboStrap\LogUtility;
1804fd306cSNickeauuse ComboStrap\MarkupRef;
1904fd306cSNickeauuse ComboStrap\MarkupPath;
20007225e5Sgerardnicouse ComboStrap\PluginUtility;
21531e725cSNickeauuse ComboStrap\TagAttributes;
22c3437056SNickeauuse ComboStrap\ThirdPartyPlugins;
2304fd306cSNickeauuse ComboStrap\Web\UrlEndpoint;
24007225e5Sgerardnico
25007225e5Sgerardnicoif (!defined('DOKU_INC')) die();
26007225e5Sgerardnico
27007225e5Sgerardnico/**
28007225e5Sgerardnico *
29007225e5Sgerardnico * A link pattern to take over the link of Dokuwiki
30007225e5Sgerardnico * and transform it as a bootstrap link
31007225e5Sgerardnico *
32007225e5Sgerardnico * The handle of the move of link is to be found in the
33007225e5Sgerardnico * admin action {@link action_plugin_combo_linkmove}
34007225e5Sgerardnico *
3504fd306cSNickeau * popular [[ wiki ]] syntax for linking notes
3604fd306cSNickeau * and makes it easy to build personal wikis,
3704fd306cSNickeau * team knowledge bases,
3804fd306cSNickeau * or something like a Second Brain or a Zettelkasten.
3904fd306cSNickeau *
40007225e5Sgerardnico */
41007225e5Sgerardnicoclass syntax_plugin_combo_link extends DokuWiki_Syntax_Plugin
42007225e5Sgerardnico{
43007225e5Sgerardnico    const TAG = 'link';
44ef295d81Sgerardnico    const COMPONENT = 'combo_link';
45007225e5Sgerardnico
465f891b7eSNickeau    /**
4785e82846SNickeau     * Disable the link component
4821913ab3SNickeau     */
4921913ab3SNickeau    const CONF_DISABLE_LINK = "disableLink";
5021913ab3SNickeau
5121913ab3SNickeau    /**
525f891b7eSNickeau     * The link Tag
53531e725cSNickeau     * a or p
545f891b7eSNickeau     */
555f891b7eSNickeau    const LINK_TAG = "linkTag";
565f891b7eSNickeau
5721913ab3SNickeau    /**
5821913ab3SNickeau     * Do the link component allows to be spawn on multilines
5921913ab3SNickeau     */
60531e725cSNickeau    const CLICKABLE_ATTRIBUTE = "clickable";
614cadd4f8SNickeau    public const ATTRIBUTE_LABEL = 'label';
624cadd4f8SNickeau    /**
634cadd4f8SNickeau     * The key of the array for the handle cache
644cadd4f8SNickeau     */
6504fd306cSNickeau    public const MARKUP_REF_ATTRIBUTE = 'ref';
6604fd306cSNickeau
674cadd4f8SNickeau    public const ATTRIBUTE_IMAGE_IN_LABEL = 'image-in-label';
684cadd4f8SNickeau
694cadd4f8SNickeau    /**
704cadd4f8SNickeau     * A link may have a title or not
714cadd4f8SNickeau     * ie
724cadd4f8SNickeau     * [[path:page]]
734cadd4f8SNickeau     * [[path:page|title]]
744cadd4f8SNickeau     * are valid
754cadd4f8SNickeau     *
764cadd4f8SNickeau     * Get the content until one of this character is found:
774cadd4f8SNickeau     *   * |
784cadd4f8SNickeau     *   * or ]]
794cadd4f8SNickeau     *   * or \n (No line break allowed, too much difficult to debug)
804cadd4f8SNickeau     *   * and not [ (for two links on the same line)
814cadd4f8SNickeau     */
824cadd4f8SNickeau    public const ENTRY_PATTERN_SINGLE_LINE = "\[\[[^\|\]]*(?=[^\n\[]*\]\])";
834cadd4f8SNickeau    public const EXIT_PATTERN = "\]\]";
844cadd4f8SNickeau
854cadd4f8SNickeau
864cadd4f8SNickeau    /**
874cadd4f8SNickeau     * Dokuwiki Link pattern ter info
884cadd4f8SNickeau     * Found in {@link \dokuwiki\Parsing\ParserMode\Internallink}
894cadd4f8SNickeau     */
904cadd4f8SNickeau    const SPECIAL_PATTERN = "\[\[.*?\]\](?!\])";
914cadd4f8SNickeau
924cadd4f8SNickeau    /**
934cadd4f8SNickeau     * The link title attribute (ie popup)
944cadd4f8SNickeau     */
954cadd4f8SNickeau    const TITLE_ATTRIBUTE = "title";
9604fd306cSNickeau    const STRETCHED_LINK = "stretched-link";
9704fd306cSNickeau    const CANONICAL = "link";
984cadd4f8SNickeau
994cadd4f8SNickeau
1004cadd4f8SNickeau    /**
1014cadd4f8SNickeau     * Parse the match of a syntax {@link DokuWiki_Syntax_Plugin} handle function
1024cadd4f8SNickeau     * @param $match
1034cadd4f8SNickeau     * @return string[] - an array with the attributes constant `ATTRIBUTE_xxxx` as key
1044cadd4f8SNickeau     *
1054cadd4f8SNickeau     * Code adapted from  {@link Doku_Handler::internallink()}
1064cadd4f8SNickeau     */
1074cadd4f8SNickeau    public static function parse($match): array
1084cadd4f8SNickeau    {
1094cadd4f8SNickeau
1104cadd4f8SNickeau        // Strip the opening and closing markup
11104fd306cSNickeau        $linkString = preg_replace(array('/^\[\[/', '/]]$/u'), '', $match);
1124cadd4f8SNickeau
1134cadd4f8SNickeau        // Split title from URL
1144cadd4f8SNickeau        $linkArray = explode('|', $linkString, 2);
1154cadd4f8SNickeau
1164cadd4f8SNickeau        // Id
11704fd306cSNickeau        $attributes[self::MARKUP_REF_ATTRIBUTE] = trim($linkArray[0]);
1184cadd4f8SNickeau
1194cadd4f8SNickeau
1204cadd4f8SNickeau        // Text or image
1214cadd4f8SNickeau        if (!isset($linkArray[1])) {
1224cadd4f8SNickeau            $attributes[self::ATTRIBUTE_LABEL] = null;
1234cadd4f8SNickeau        } else {
1244cadd4f8SNickeau            // An image in the title
12504fd306cSNickeau            if (preg_match('/^{{[^}]+}}$/', $linkArray[1])) {
1264cadd4f8SNickeau                // If the title is an image, convert it to an array containing the image details
1274cadd4f8SNickeau                $attributes[self::ATTRIBUTE_IMAGE_IN_LABEL] = Doku_Handler_Parse_Media($linkArray[1]);
1284cadd4f8SNickeau            } else {
1294cadd4f8SNickeau                $attributes[self::ATTRIBUTE_LABEL] = $linkArray[1];
1304cadd4f8SNickeau            }
1314cadd4f8SNickeau        }
1324cadd4f8SNickeau
1334cadd4f8SNickeau        return $attributes;
1344cadd4f8SNickeau
1354cadd4f8SNickeau    }
13621913ab3SNickeau
1375f891b7eSNickeau
138007225e5Sgerardnico    /**
139007225e5Sgerardnico     * Syntax Type.
140007225e5Sgerardnico     *
141007225e5Sgerardnico     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
142007225e5Sgerardnico     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
143007225e5Sgerardnico     */
144007225e5Sgerardnico    function getType()
145007225e5Sgerardnico    {
146007225e5Sgerardnico        return 'substition';
147007225e5Sgerardnico    }
148007225e5Sgerardnico
149007225e5Sgerardnico    /**
150007225e5Sgerardnico     * How Dokuwiki will add P element
151007225e5Sgerardnico     *
152007225e5Sgerardnico     *  * 'normal' - The plugin can be used inside paragraphs
153007225e5Sgerardnico     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
154007225e5Sgerardnico     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
155007225e5Sgerardnico     *
156007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::getPType()
157007225e5Sgerardnico     */
158007225e5Sgerardnico    function getPType()
159007225e5Sgerardnico    {
160007225e5Sgerardnico        return 'normal';
161007225e5Sgerardnico    }
162007225e5Sgerardnico
163007225e5Sgerardnico    /**
164007225e5Sgerardnico     * @return array
165007225e5Sgerardnico     * Allow which kind of plugin inside
166007225e5Sgerardnico     *
167007225e5Sgerardnico     * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
168007225e5Sgerardnico     * because we manage self the content and we call self the parser
169007225e5Sgerardnico     */
1704cadd4f8SNickeau    function getAllowedTypes(): array
171007225e5Sgerardnico    {
172007225e5Sgerardnico        return array('substition', 'formatting', 'disabled');
173007225e5Sgerardnico    }
174007225e5Sgerardnico
17537748cd8SNickeau    /**
17637748cd8SNickeau     * @param string $mode
17737748cd8SNickeau     * @return bool
17837748cd8SNickeau     * Accepts inside
17937748cd8SNickeau     */
1804cadd4f8SNickeau    public function accepts($mode): bool
1815f891b7eSNickeau    {
1825f891b7eSNickeau        /**
1835f891b7eSNickeau         * To avoid that the description if it contains a link
1845f891b7eSNickeau         * will be taken by the links mode
1855f891b7eSNickeau         *
1865f891b7eSNickeau         * For instance, [[https://hallo|https://hallo]] will send https://hallo
1875f891b7eSNickeau         * to the external link mode
1885f891b7eSNickeau         */
1895f891b7eSNickeau        $linkModes = [
1905f891b7eSNickeau            "externallink",
1915f891b7eSNickeau            "locallink",
1925f891b7eSNickeau            "internallink",
1935f891b7eSNickeau            "interwikilink",
1945f891b7eSNickeau            "emaillink",
195fc45fbf7Sgerardnico            "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description
1965f891b7eSNickeau            //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet
1975f891b7eSNickeau            //"emphasis_close",
1985f891b7eSNickeau            //"acrnonym"
1995f891b7eSNickeau        ];
2005f891b7eSNickeau        if (in_array($mode, $linkModes)) {
2015f891b7eSNickeau            return false;
2025f891b7eSNickeau        } else {
2035f891b7eSNickeau            return true;
2045f891b7eSNickeau        }
2055f891b7eSNickeau    }
2065f891b7eSNickeau
2075f891b7eSNickeau
208007225e5Sgerardnico    /**
209007225e5Sgerardnico     * @see Doku_Parser_Mode::getSort()
210007225e5Sgerardnico     * The mode with the lowest sort number will win out
211007225e5Sgerardnico     */
212007225e5Sgerardnico    function getSort()
213007225e5Sgerardnico    {
214e8b2ff59SNickeau        /**
215e8b2ff59SNickeau         * It should be less than the number
216e8b2ff59SNickeau         * at {@link \dokuwiki\Parsing\ParserMode\Internallink::getSort}
217e8b2ff59SNickeau         * and the like
218e8b2ff59SNickeau         *
219e8b2ff59SNickeau         * For whatever reason, the number below should be less than 100,
220e8b2ff59SNickeau         * otherwise on windows with DokuWiki Stick, the link syntax may be not taken
221e8b2ff59SNickeau         * into account
222e8b2ff59SNickeau         */
223e8b2ff59SNickeau        return 99;
224007225e5Sgerardnico    }
225007225e5Sgerardnico
226007225e5Sgerardnico
227007225e5Sgerardnico    function connectTo($mode)
228007225e5Sgerardnico    {
229d262537cSgerardnico
23037748cd8SNickeau        if (!$this->getConf(self::CONF_DISABLE_LINK, false)
23137748cd8SNickeau            &&
23237748cd8SNickeau            $mode !== PluginUtility::getModeFromPluginName(ThirdPartyPlugins::IMAGE_MAPPING_NAME)
23337748cd8SNickeau        ) {
23437748cd8SNickeau
2354cadd4f8SNickeau            $pattern = self::ENTRY_PATTERN_SINGLE_LINE;
2369337a630SNickeau            $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
23737748cd8SNickeau
23821913ab3SNickeau        }
239d262537cSgerardnico
240007225e5Sgerardnico    }
241007225e5Sgerardnico
2425f891b7eSNickeau    public function postConnect()
2435f891b7eSNickeau    {
24421913ab3SNickeau        if (!$this->getConf(self::CONF_DISABLE_LINK, false)) {
2454cadd4f8SNickeau            $this->Lexer->addExitPattern(self::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent()));
2465f891b7eSNickeau        }
24721913ab3SNickeau    }
2485f891b7eSNickeau
249007225e5Sgerardnico
250007225e5Sgerardnico    /**
251007225e5Sgerardnico     * The handler for an internal link
252007225e5Sgerardnico     * based on `internallink` in {@link Doku_Handler}
253007225e5Sgerardnico     * The handler call the good renderer in {@link Doku_Renderer_xhtml} with
254007225e5Sgerardnico     * the parameters (ie for instance internallink)
255007225e5Sgerardnico     * @param string $match
256007225e5Sgerardnico     * @param int $state
257007225e5Sgerardnico     * @param int $pos
258007225e5Sgerardnico     * @param Doku_Handler $handler
259007225e5Sgerardnico     * @return array|bool
260007225e5Sgerardnico     */
261007225e5Sgerardnico    function handle($match, $state, $pos, Doku_Handler $handler)
262007225e5Sgerardnico    {
263007225e5Sgerardnico
2645f891b7eSNickeau        switch ($state) {
2655f891b7eSNickeau            case DOKU_LEXER_ENTER:
2664cadd4f8SNickeau                $parsedArray = self::parse($match);
2674cadd4f8SNickeau                $htmlAttributes = TagAttributes::createEmpty(self::TAG);
26804fd306cSNickeau
2694cadd4f8SNickeau                /**
27004fd306cSNickeau                 * The markup ref needs to be passed to the
27104fd306cSNickeau                 * instructions stack (because we support link with a variable as markup ref
27204fd306cSNickeau                 * via a {@link syntax_plugin_combo_fragment} in a {@link syntax_plugin_combo_iterator}
27304fd306cSNickeau                 *
27404fd306cSNickeau                 * The variable is replaced in the {@link syntax_plugin_combo_link::render()}
27504fd306cSNickeau                 * at runtime while rendering
2764cadd4f8SNickeau                 */
27704fd306cSNickeau                $markupRef = $parsedArray[self::MARKUP_REF_ATTRIBUTE];
27804fd306cSNickeau                if ($markupRef !== null) {
2794fbd4ae2SNico                    /**
2804fbd4ae2SNico                     * If the Rel is a wiki link, we make the path absolute and not relative
2814fbd4ae2SNico                     * (this is for the {@link \ComboStrap\FetcherPageBundler)}}
2824fbd4ae2SNico                     * otherwise the links are not good and are seen as non-existent
2834fbd4ae2SNico                     */
2844fbd4ae2SNico                    try {
2854fbd4ae2SNico                        $markupRefObject = MarkupRef::createLinkFromRef($markupRef);
2864fbd4ae2SNico                        $scheme = $markupRefObject->getSchemeType();
2874fbd4ae2SNico                        if ($scheme === MarkupRef::WIKI_URI) {
288738ed353SNico
289738ed353SNico                            /**
290738ed353SNico                             * Properties?
291738ed353SNico                             */
292738ed353SNico                            $newProperties = [];
293738ed353SNico                            $url = $markupRefObject->getUrl();
294738ed353SNico                            foreach (array_keys($url->getQueryProperties()) as $propertyName) {
295738ed353SNico                                if ($propertyName === "id") {
296738ed353SNico                                    continue;
297738ed353SNico                                }
298738ed353SNico                                $queryPropertyValue = $url->getQueryPropertyValue($propertyName);
299738ed353SNico                                if (is_array($queryPropertyValue)) {
300738ed353SNico                                    foreach ($queryPropertyValue as $arrayValue) {
301738ed353SNico                                        $newProperties[] = $propertyName . "=" . $arrayValue;
302738ed353SNico                                    }
303738ed353SNico                                } else {
304738ed353SNico                                    $newProperties[] = $propertyName . "=" . $queryPropertyValue;
305738ed353SNico                                }
306738ed353SNico                            }
3074fbd4ae2SNico                            $markupRef = $markupRefObject->getPath()->toAbsoluteId();
308738ed353SNico                            if (count($newProperties) > 0) {
309738ed353SNico                                $queryString = implode("&", $newProperties);
310738ed353SNico                                $markupRef .= "?" . $queryString;
311738ed353SNico                            }
312738ed353SNico                            try {
313738ed353SNico                                $markupRef .= "#" . $url->getFragment();
314738ed353SNico                            } catch (ExceptionNotFound $e) {
315738ed353SNico                                // no fragment
316738ed353SNico                            }
3174fbd4ae2SNico                        }
3184fbd4ae2SNico                    } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) {
319738ed353SNico                        // not a valid ref
3204fbd4ae2SNico                    }
32104fd306cSNickeau                    $htmlAttributes->addComponentAttributeValue(self::MARKUP_REF_ATTRIBUTE, $markupRef);
3224cadd4f8SNickeau                }
3234cadd4f8SNickeau
3244cadd4f8SNickeau
3254cadd4f8SNickeau                /**
3264cadd4f8SNickeau                 * Extra HTML attribute
3274cadd4f8SNickeau                 */
328531e725cSNickeau                $callStack = CallStack::createFromHandler($handler);
329531e725cSNickeau                $parent = $callStack->moveToParent();
330007225e5Sgerardnico                $parentName = "";
3314cadd4f8SNickeau                if ($parent !== false) {
332531e725cSNickeau
333531e725cSNickeau                    /**
334531e725cSNickeau                     * Button Link
335531e725cSNickeau                     * Getting the attributes
336531e725cSNickeau                     */
337531e725cSNickeau                    $parentName = $parent->getTagName();
33804fd306cSNickeau                    if ($parentName == ButtonTag::MARKUP_LONG) {
3394cadd4f8SNickeau                        $htmlAttributes->mergeWithCallStackArray($parent->getAttributes());
34021913ab3SNickeau                    }
341531e725cSNickeau
342531e725cSNickeau                    /**
343531e725cSNickeau                     * Searching Clickable parent
344531e725cSNickeau                     */
345531e725cSNickeau                    $maxLevel = 3;
346531e725cSNickeau                    $level = 0;
347531e725cSNickeau                    while (
348531e725cSNickeau                        $parent != false &&
349531e725cSNickeau                        !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) &&
350531e725cSNickeau                        $level < $maxLevel
351531e725cSNickeau                    ) {
352531e725cSNickeau                        $parent = $callStack->moveToParent();
353531e725cSNickeau                        $level++;
3545f891b7eSNickeau                    }
35504fd306cSNickeau                    if ($parent !== false) {
356531e725cSNickeau                        if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) {
35704fd306cSNickeau                            $htmlAttributes->addClassName(self::STRETCHED_LINK);
358531e725cSNickeau                            $parent->addClassName("position-relative");
359531e725cSNickeau                            $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE);
3605f891b7eSNickeau                        }
36121913ab3SNickeau                    }
36221913ab3SNickeau
363531e725cSNickeau                }
3644cadd4f8SNickeau                $returnedArray[PluginUtility::STATE] = $state;
3654cadd4f8SNickeau                $returnedArray[PluginUtility::ATTRIBUTES] = $htmlAttributes->toCallStackArray();
3664cadd4f8SNickeau                $returnedArray[PluginUtility::CONTEXT] = $parentName;
3674cadd4f8SNickeau                return $returnedArray;
3684cadd4f8SNickeau
3695f891b7eSNickeau            case DOKU_LEXER_UNMATCHED:
3705f891b7eSNickeau
37132b85071SNickeau                $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
3725f891b7eSNickeau                /**
37332b85071SNickeau                 * Delete the separator `|` between the ref and the description if any
3745f891b7eSNickeau                 */
3751fa8c418SNickeau                $tag = CallStack::createFromHandler($handler);
3761fa8c418SNickeau                $parent = $tag->moveToParent();
3771fa8c418SNickeau                if ($parent->getTagName() == self::TAG) {
3785f891b7eSNickeau                    if (strpos($match, '|') === 0) {
37932b85071SNickeau                        $data[PluginUtility::PAYLOAD] = substr($match, 1);
3805f891b7eSNickeau                    }
381007225e5Sgerardnico                }
38232b85071SNickeau                return $data;
383007225e5Sgerardnico
3845f891b7eSNickeau            case DOKU_LEXER_EXIT:
38504fd306cSNickeau
386531e725cSNickeau                $callStack = CallStack::createFromHandler($handler);
387531e725cSNickeau                $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
3884cadd4f8SNickeau
3895f891b7eSNickeau                $openingAttributes = $openingTag->getAttributes();
390531e725cSNickeau                $openingPosition = $openingTag->getKey();
3915f891b7eSNickeau
392531e725cSNickeau                $callStack->moveToEnd();
393531e725cSNickeau                $previousCall = $callStack->previous();
394531e725cSNickeau                $previousCallPosition = $previousCall->getKey();
395531e725cSNickeau                $previousCallContent = $previousCall->getCapturedContent();
396531e725cSNickeau
3974cadd4f8SNickeau                /**
3984cadd4f8SNickeau                 * Link label
3994cadd4f8SNickeau                 * is set if there is no content
4004cadd4f8SNickeau                 * between enter and exit node
4014cadd4f8SNickeau                 */
4024cadd4f8SNickeau                $linkLabel = "";
403531e725cSNickeau                if (
404531e725cSNickeau                    $openingPosition == $previousCallPosition // ie [[id]]
405531e725cSNickeau                    ||
406531e725cSNickeau                    ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]]
407531e725cSNickeau                ) {
4085f891b7eSNickeau                    // There is no name
40904fd306cSNickeau                    $markupRef = $openingTag->getAttribute(self::MARKUP_REF_ATTRIBUTE);
41004fd306cSNickeau                    if ($markupRef !== null) {
41104fd306cSNickeau                        try {
41204fd306cSNickeau                            $linkLabel = LinkMarkup::createFromRef($markupRef)
41304fd306cSNickeau                                ->getDefaultLabel();
41404fd306cSNickeau                        } catch (ExceptionCompile $e) {
41504fd306cSNickeau                            LogUtility::error("No default Label can be defined. Error while parsing the markup ref ($markupRef). Error: {$e->getMessage()}", self::CANONICAL, $e);
41604fd306cSNickeau                        }
41704fd306cSNickeau
4184cadd4f8SNickeau                    }
4195f891b7eSNickeau                }
4205f891b7eSNickeau                return array(
4215f891b7eSNickeau                    PluginUtility::STATE => $state,
4225f891b7eSNickeau                    PluginUtility::ATTRIBUTES => $openingAttributes,
4234cadd4f8SNickeau                    PluginUtility::PAYLOAD => $linkLabel,
42485e82846SNickeau                    PluginUtility::CONTEXT => $openingTag->getContext()
4255f891b7eSNickeau                );
4265f891b7eSNickeau        }
4275f891b7eSNickeau        return true;
4285f891b7eSNickeau
429007225e5Sgerardnico
430007225e5Sgerardnico    }
431007225e5Sgerardnico
432007225e5Sgerardnico    /**
433007225e5Sgerardnico     * Render the output
434007225e5Sgerardnico     * @param string $format
435007225e5Sgerardnico     * @param Doku_Renderer $renderer
436007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
437007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
438007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
439007225e5Sgerardnico     *
440007225e5Sgerardnico     *
441007225e5Sgerardnico     */
442c3437056SNickeau    function render($format, Doku_Renderer $renderer, $data): bool
443007225e5Sgerardnico    {
444007225e5Sgerardnico        // The data
44504fd306cSNickeau        $state = $data[PluginUtility::STATE];
446007225e5Sgerardnico        switch ($format) {
447007225e5Sgerardnico            case 'xhtml':
448007225e5Sgerardnico
449007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
450007225e5Sgerardnico                /**
45119b0880dSgerardnico                 * Cache problem may occurs while releasing
452007225e5Sgerardnico                 */
453007225e5Sgerardnico                if (isset($data[PluginUtility::ATTRIBUTES])) {
454531e725cSNickeau                    $callStackAttributes = $data[PluginUtility::ATTRIBUTES];
455007225e5Sgerardnico                } else {
456531e725cSNickeau                    $callStackAttributes = $data;
457007225e5Sgerardnico                }
4585f891b7eSNickeau
45904fd306cSNickeau                PluginUtility::getSnippetManager()->attachCssInternalStyleSheet(self::TAG);
4605f891b7eSNickeau
4615f891b7eSNickeau                switch ($state) {
4625f891b7eSNickeau                    case DOKU_LEXER_ENTER:
463531e725cSNickeau                        $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG);
4644cadd4f8SNickeau
46504fd306cSNickeau                        $markupRef = $tagAttributes->getValueAndRemove(self::MARKUP_REF_ATTRIBUTE);
46604fd306cSNickeau                        if ($markupRef === null) {
46704fd306cSNickeau                            $message = "Internal Error: A link reference was not found";
46804fd306cSNickeau                            LogUtility::internalError($message);
46904fd306cSNickeau                            $renderer->doc .= LogUtility::wrapInRedForHtml($message);
4704cadd4f8SNickeau                            return false;
4714cadd4f8SNickeau                        }
47204fd306cSNickeau                        try {
47304fd306cSNickeau                            $markupRef = syntax_plugin_combo_variable::replaceVariablesWithValuesFromContext($markupRef);
47404fd306cSNickeau                            $markupLink = LinkMarkup::createFromRef($markupRef);
47504fd306cSNickeau                            $markupAttributes = $markupLink->toAttributes();
47604fd306cSNickeau                        } catch (ExceptionCompile $e) {
47704fd306cSNickeau                            // uncomment to get the original error stack trace in dev
47804fd306cSNickeau                            // and see where the exception comes from
47904fd306cSNickeau                            // Don't forget to comment back
48004fd306cSNickeau//                            if (PluginUtility::isDevOrTest()) {
48104fd306cSNickeau//                                throw new ExceptionRuntime("Error on markup ref", self::TAG, 0, $e);
48204fd306cSNickeau//                            }
48304fd306cSNickeau
48404fd306cSNickeau                            /**
48504fd306cSNickeau                             * Error. Example: unknown inter-wiki ...
48604fd306cSNickeau                             * We still create the a to be xhtml compliante
48704fd306cSNickeau                             */
48804fd306cSNickeau                            $url = UrlEndpoint::createSupportUrl();
48904fd306cSNickeau                            $markupAttributes = TagAttributes::createEmpty()
49004fd306cSNickeau                                ->addOutputAttributeValue("href", $url->toString())
49104fd306cSNickeau                                ->addClassName(LinkMarkup::getHtmlClassNotExist());
49204fd306cSNickeau                            $renderer->doc .= $markupAttributes->toHtmlEnterTag("a") . $e->getMessage();
49304fd306cSNickeau
49404fd306cSNickeau                            LogUtility::warning($e->getMessage(), "link", $e);
49504fd306cSNickeau                            return false;
4964cadd4f8SNickeau
4974cadd4f8SNickeau                        }
49804fd306cSNickeau                        // markup attributes is leading because it has already output attribute such as href
49904fd306cSNickeau                        $markupAttributes->mergeWithCallStackArray($tagAttributes->toCallStackArray());
50004fd306cSNickeau                        $tagAttributes = $markupAttributes;
50104fd306cSNickeau
502d262537cSgerardnico
50319b0880dSgerardnico                        /**
5045f891b7eSNickeau                         * Extra styling
50519b0880dSgerardnico                         */
5065f891b7eSNickeau                        $parentTag = $data[PluginUtility::CONTEXT];
5074cadd4f8SNickeau                        $htmlPrefix = "";
5089f4383e9Sgerardnico                        switch ($parentTag) {
50921913ab3SNickeau                            /**
51021913ab3SNickeau                             * Button link
51121913ab3SNickeau                             */
51204fd306cSNickeau                            case ButtonTag::MARKUP_LONG:
5134cadd4f8SNickeau                                $tagAttributes->addOutputAttributeValue("role", "button");
51404fd306cSNickeau                                ButtonTag::processButtonAttributesToHtmlAttributes($tagAttributes);
5159f4383e9Sgerardnico                                break;
51604fd306cSNickeau                            case DropDownTag::TAG:
5174cadd4f8SNickeau                                $tagAttributes->addClassName("dropdown-item");
5184cadd4f8SNickeau                                break;
5194cadd4f8SNickeau                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
5204cadd4f8SNickeau                                $tagAttributes->addClassName("navbar-link");
5214cadd4f8SNickeau                                $htmlPrefix = '<div class="navbar-nav">';
5224cadd4f8SNickeau                                break;
5234cadd4f8SNickeau                            case syntax_plugin_combo_navbargroup::COMPONENT:
5244cadd4f8SNickeau                                $tagAttributes->addClassName("nav-link");
5254cadd4f8SNickeau                                $htmlPrefix = '<li class="nav-item">';
5264cadd4f8SNickeau                                break;
5274cadd4f8SNickeau                            default:
5285f891b7eSNickeau                            case syntax_plugin_combo_badge::TAG:
5299f4383e9Sgerardnico                            case syntax_plugin_combo_cite::TAG:
5309337a630SNickeau                            case syntax_plugin_combo_contentlistitem::DOKU_TAG:
5319f4383e9Sgerardnico                            case syntax_plugin_combo_preformatted::TAG:
5329f4383e9Sgerardnico                                break;
5330a517624Sgerardnico
5349f4383e9Sgerardnico                        }
5359f4383e9Sgerardnico
53619b0880dSgerardnico                        /**
53719b0880dSgerardnico                         * Add it to the rendering
53819b0880dSgerardnico                         */
5394cadd4f8SNickeau                        $renderer->doc .= $htmlPrefix . $tagAttributes->toHtmlEnterTag("a");
5405f891b7eSNickeau                        break;
5415f891b7eSNickeau                    case DOKU_LEXER_UNMATCHED:
54232b85071SNickeau                        $renderer->doc .= PluginUtility::renderUnmatched($data);
5435f891b7eSNickeau                        break;
5445f891b7eSNickeau                    case DOKU_LEXER_EXIT:
5455f891b7eSNickeau
5465f891b7eSNickeau                        // if there is no link name defined, we get the name as ref in the payload
5475f891b7eSNickeau                        // otherwise null string
54870bbd7f1Sgerardnico                        $renderer->doc .= $data[PluginUtility::PAYLOAD] ?? '';
5495f891b7eSNickeau
550e3d0019cSgerardnico                        // Close the link
55185e82846SNickeau                        $renderer->doc .= "</a>";
552e3d0019cSgerardnico
553e3d0019cSgerardnico                        // Close the html wrapper element
5545f891b7eSNickeau                        $context = $data[PluginUtility::CONTEXT];
5555f891b7eSNickeau                        switch ($context) {
5565f891b7eSNickeau                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
5575f891b7eSNickeau                                $renderer->doc .= '</div>';
5585f891b7eSNickeau                                break;
5595f891b7eSNickeau                            case syntax_plugin_combo_navbargroup::COMPONENT:
5605f891b7eSNickeau                                $renderer->doc .= '</li>';
5615f891b7eSNickeau                                break;
5625f891b7eSNickeau                        }
5635f891b7eSNickeau
5645f891b7eSNickeau                }
565007225e5Sgerardnico                return true;
5665f891b7eSNickeau            case 'metadata':
567007225e5Sgerardnico
5684cadd4f8SNickeau                /**
5694cadd4f8SNickeau                 * @var Doku_Renderer_metadata $renderer
5704cadd4f8SNickeau                 */
5714cadd4f8SNickeau                switch ($state) {
5724cadd4f8SNickeau                    case DOKU_LEXER_ENTER:
573007225e5Sgerardnico                        /**
574007225e5Sgerardnico                         * Keep track of the backlinks ie meta['relation']['references']
575007225e5Sgerardnico                         * @var Doku_Renderer_metadata $renderer
576007225e5Sgerardnico                         */
5774cadd4f8SNickeau                        $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
57804fd306cSNickeau
57904fd306cSNickeau                        $markupRef = $tagAttributes->getValue(self::MARKUP_REF_ATTRIBUTE);
58004fd306cSNickeau                        if ($markupRef === null) {
58104fd306cSNickeau                            LogUtility::internalError("The markup ref was not found for a link.");
5824cadd4f8SNickeau                            return false;
583007225e5Sgerardnico                        }
58404fd306cSNickeau                        try {
58504fd306cSNickeau                            $type = MarkupRef::createLinkFromRef($markupRef)
58604fd306cSNickeau                                ->getSchemeType();
58704fd306cSNickeau                        } catch (ExceptionCompile $e) {
58804fd306cSNickeau                            LogUtility::error("The markup ref could not be parsed. Error:{$e->getMessage()}");
58904fd306cSNickeau                            return false;
59004fd306cSNickeau                        }
5914cadd4f8SNickeau                        $name = $tagAttributes->getValue(self::ATTRIBUTE_LABEL);
5929f4383e9Sgerardnico
5934cadd4f8SNickeau                        switch ($type) {
5944cadd4f8SNickeau                            case MarkupRef::WIKI_URI:
5954cadd4f8SNickeau                                /**
5964cadd4f8SNickeau                                 * The relative link should be passed (ie the original)
5974cadd4f8SNickeau                                 * Dokuwiki has a default description
5984cadd4f8SNickeau                                 * We can't pass empty or the array(title), it does not work
5994cadd4f8SNickeau                                 */
6004cadd4f8SNickeau                                $descriptionToDelete = "b";
60104fd306cSNickeau                                $renderer->internallink($markupRef, $descriptionToDelete);
6024cadd4f8SNickeau                                $renderer->doc = substr($renderer->doc, 0, -strlen($descriptionToDelete));
6034cadd4f8SNickeau                                break;
6044cadd4f8SNickeau                            case MarkupRef::WEB_URI:
60504fd306cSNickeau                                $renderer->externallink($markupRef, $name);
6064cadd4f8SNickeau                                break;
6074cadd4f8SNickeau                            case MarkupRef::LOCAL_URI:
60804fd306cSNickeau                                $renderer->locallink($markupRef, $name);
6094cadd4f8SNickeau                                break;
6104cadd4f8SNickeau                            case MarkupRef::EMAIL_URI:
61104fd306cSNickeau                                $renderer->emaillink($markupRef, $name);
6124cadd4f8SNickeau                                break;
6134cadd4f8SNickeau                            case MarkupRef::INTERWIKI_URI:
61404fd306cSNickeau                                $interWikiSplit = preg_split("/>/", $markupRef);
61504fd306cSNickeau                                $renderer->interwikilink($markupRef, $name, $interWikiSplit[0], $interWikiSplit[1]);
6164cadd4f8SNickeau                                break;
6174cadd4f8SNickeau                            case MarkupRef::WINDOWS_SHARE_URI:
61804fd306cSNickeau                                $renderer->windowssharelink($markupRef, $name);
6194cadd4f8SNickeau                                break;
6204cadd4f8SNickeau                            case MarkupRef::VARIABLE_URI:
6214cadd4f8SNickeau                                // No backlinks for link template
6224cadd4f8SNickeau                                break;
6234cadd4f8SNickeau                            default:
62404fd306cSNickeau                                LogUtility::msg("The markup reference ({$markupRef}) with the type $type was not processed into the metadata");
6259f4383e9Sgerardnico                        }
626007225e5Sgerardnico
627007225e5Sgerardnico                        return true;
6284cadd4f8SNickeau                    case DOKU_LEXER_UNMATCHED:
6294cadd4f8SNickeau                        $renderer->doc .= PluginUtility::renderUnmatched($data);
6304cadd4f8SNickeau                        break;
6315f891b7eSNickeau                }
632007225e5Sgerardnico                break;
633007225e5Sgerardnico
634531e725cSNickeau            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
6355f891b7eSNickeau
63604fd306cSNickeau                if ($state === DOKU_LEXER_ENTER) {
637007225e5Sgerardnico                    /**
638007225e5Sgerardnico                     *
639007225e5Sgerardnico                     * @var renderer_plugin_combo_analytics $renderer
640007225e5Sgerardnico                     */
6414cadd4f8SNickeau                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
64204fd306cSNickeau                    $ref = $tagAttributes->getValue(self::MARKUP_REF_ATTRIBUTE);
64304fd306cSNickeau                    try {
64404fd306cSNickeau                        $markupRef = LinkMarkup::createFromRef($ref);
64504fd306cSNickeau                    } catch (ExceptionCompile $e) {
64604fd306cSNickeau                        LogUtility::error("Error while parsing the ref ($ref). Error: {$e->getMessage()}. No further analytics.");
6474cadd4f8SNickeau                        return false;
6484cadd4f8SNickeau                    }
64904fd306cSNickeau                    $refType = $markupRef->getMarkupRef()->getSchemeType();
6504cadd4f8SNickeau
6514cadd4f8SNickeau
6524cadd4f8SNickeau                    /**
6534cadd4f8SNickeau                     * @param array $stats
6544cadd4f8SNickeau                     * Calculate internal link statistics
6554cadd4f8SNickeau                     */
6564cadd4f8SNickeau                    $stats = &$renderer->stats;
6574cadd4f8SNickeau                    switch ($refType) {
6584cadd4f8SNickeau
6594cadd4f8SNickeau                        case MarkupRef::WIKI_URI:
6604cadd4f8SNickeau
6614cadd4f8SNickeau                            /**
6624cadd4f8SNickeau                             * Internal link count
6634cadd4f8SNickeau                             */
66404fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT, $stats)) {
66504fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT] = 0;
6664cadd4f8SNickeau                            }
66704fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT]++;
6684cadd4f8SNickeau
6694cadd4f8SNickeau
6704cadd4f8SNickeau                            /**
6714cadd4f8SNickeau                             * Broken link ?
6724cadd4f8SNickeau                             */
67304fd306cSNickeau                            try {
67404fd306cSNickeau                                $path = $markupRef->getMarkupRef()->getPath();
67504fd306cSNickeau                                $linkedPage = MarkupPath::createPageFromPathObject($path);
67604fd306cSNickeau                                if (!FileSystems::exists($path)) {
67770bbd7f1Sgerardnico                                    $internalLinkBroken = $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT] ?? 0;
67870bbd7f1Sgerardnico                                    $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT] = $internalLinkBroken + 1;
67904fd306cSNickeau                                    $stats[renderer_plugin_combo_analytics::INFO][] = "The internal linked page `{$linkedPage}` does not exist";
68004fd306cSNickeau                                }
68104fd306cSNickeau                            } catch (ExceptionNotFound $e) {
68204fd306cSNickeau                                // no local path
6834cadd4f8SNickeau                            }
6844cadd4f8SNickeau
6854cadd4f8SNickeau                            /**
6864cadd4f8SNickeau                             * Calculate link distance
6874cadd4f8SNickeau                             */
6884cadd4f8SNickeau                            global $ID;
68904fd306cSNickeau                            $id = $linkedPage->getWikiId();
6904cadd4f8SNickeau                            $a = explode(':', getNS($ID));
6914cadd4f8SNickeau                            $b = explode(':', getNS($id));
692*ac4b853aSNico                            while (isset($a[0]) && isset($b[0]) && $a[0] == $b[0]) {
6934cadd4f8SNickeau                                array_shift($a);
6944cadd4f8SNickeau                                array_shift($b);
6954cadd4f8SNickeau                            }
6964cadd4f8SNickeau                            $length = count($a) + count($b);
69704fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_DISTANCE][] = $length;
6984cadd4f8SNickeau                            break;
6994cadd4f8SNickeau
7004cadd4f8SNickeau                        case MarkupRef::WEB_URI:
7014cadd4f8SNickeau
70204fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT, $stats)) {
70304fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT] = 0;
7044cadd4f8SNickeau                            }
70504fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT]++;
7064cadd4f8SNickeau                            break;
7074cadd4f8SNickeau
7084cadd4f8SNickeau                        case MarkupRef::LOCAL_URI:
7094cadd4f8SNickeau
71004fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::LOCAL_LINK_COUNT, $stats)) {
71104fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::LOCAL_LINK_COUNT] = 0;
7124cadd4f8SNickeau                            }
71304fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::LOCAL_LINK_COUNT]++;
7144cadd4f8SNickeau                            break;
7154cadd4f8SNickeau
7164cadd4f8SNickeau                        case MarkupRef::INTERWIKI_URI:
7174cadd4f8SNickeau
71804fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT, $stats)) {
71904fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT] = 0;
7204cadd4f8SNickeau                            }
72104fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT]++;
7224cadd4f8SNickeau                            break;
7234cadd4f8SNickeau
7244cadd4f8SNickeau                        case MarkupRef::EMAIL_URI:
7254cadd4f8SNickeau
72604fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::EMAIL_COUNT, $stats)) {
72704fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::EMAIL_COUNT] = 0;
7284cadd4f8SNickeau                            }
72904fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::EMAIL_COUNT]++;
7304cadd4f8SNickeau                            break;
7314cadd4f8SNickeau
7324cadd4f8SNickeau                        case MarkupRef::WINDOWS_SHARE_URI:
7334cadd4f8SNickeau
73404fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT, $stats)) {
73504fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT] = 0;
7364cadd4f8SNickeau                            }
73704fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT]++;
7384cadd4f8SNickeau                            break;
7394cadd4f8SNickeau
7404cadd4f8SNickeau                        case MarkupRef::VARIABLE_URI:
7414cadd4f8SNickeau
74204fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT, $stats)) {
74304fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT] = 0;
7444cadd4f8SNickeau                            }
74504fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT]++;
7464cadd4f8SNickeau                            break;
7474cadd4f8SNickeau
7484cadd4f8SNickeau                        default:
7494cadd4f8SNickeau
7504cadd4f8SNickeau                            LogUtility::msg("The link `{$ref}` with the type ($refType)  is not taken into account into the statistics");
7514cadd4f8SNickeau
7524cadd4f8SNickeau                    }
7534cadd4f8SNickeau
7544cadd4f8SNickeau
755007225e5Sgerardnico                    break;
7565f891b7eSNickeau                }
757007225e5Sgerardnico
758007225e5Sgerardnico        }
75904fd306cSNickeau
760007225e5Sgerardnico        return false;
761007225e5Sgerardnico    }
762007225e5Sgerardnico
763007225e5Sgerardnico
7644cadd4f8SNickeau    /**
7654cadd4f8SNickeau     * Utility function to add a link into the callstack
7664cadd4f8SNickeau     * @param CallStack $callStack
7674cadd4f8SNickeau     * @param TagAttributes $tagAttributes
7684cadd4f8SNickeau     */
76904fd306cSNickeau    public
77004fd306cSNickeau    static function addOpenLinkTagInCallStack(CallStack $callStack, TagAttributes $tagAttributes)
7714cadd4f8SNickeau    {
7724cadd4f8SNickeau        $parent = $callStack->moveToParent();
7734cadd4f8SNickeau        $context = "";
7744cadd4f8SNickeau        $attributes = $tagAttributes->toCallStackArray();
7754cadd4f8SNickeau        if ($parent !== false) {
7764cadd4f8SNickeau            $context = $parent->getTagName();
77704fd306cSNickeau            if ($context === ButtonTag::MARKUP_LONG) {
7784cadd4f8SNickeau                // the link takes by default the data from the button
7794cadd4f8SNickeau                $parentAttributes = $parent->getAttributes();
7804cadd4f8SNickeau                if ($parentAttributes !== null) {
7814cadd4f8SNickeau                    $attributes = ArrayUtility::mergeByValue($parentAttributes, $attributes);
7824cadd4f8SNickeau                }
7834cadd4f8SNickeau            }
7844cadd4f8SNickeau        }
7854cadd4f8SNickeau        $callStack->appendCallAtTheEnd(
7864cadd4f8SNickeau            Call::createComboCall(
7874cadd4f8SNickeau                syntax_plugin_combo_link::TAG,
7884cadd4f8SNickeau                DOKU_LEXER_ENTER,
7894cadd4f8SNickeau                $attributes,
7904cadd4f8SNickeau                $context
7914cadd4f8SNickeau            ));
7924cadd4f8SNickeau    }
7934cadd4f8SNickeau
79404fd306cSNickeau    public
79504fd306cSNickeau    static function addExitLinkTagInCallStack(CallStack $callStack)
7964cadd4f8SNickeau    {
7974cadd4f8SNickeau        $callStack->appendCallAtTheEnd(
7984cadd4f8SNickeau            Call::createComboCall(
7994cadd4f8SNickeau                syntax_plugin_combo_link::TAG,
8004cadd4f8SNickeau                DOKU_LEXER_EXIT
8014cadd4f8SNickeau            ));
8024cadd4f8SNickeau    }
803007225e5Sgerardnico}
804007225e5Sgerardnico
805