xref: /plugin/combo/syntax/link.php (revision 4fbd4ae271d755979dab8bbc9a664893487d9d34)
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) {
279*4fbd4ae2SNico                    /**
280*4fbd4ae2SNico                     * If the Rel is a wiki link, we make the path absolute and not relative
281*4fbd4ae2SNico                     * (this is for the {@link \ComboStrap\FetcherPageBundler)}}
282*4fbd4ae2SNico                     * otherwise the links are not good and are seen as non-existent
283*4fbd4ae2SNico                     */
284*4fbd4ae2SNico                    try {
285*4fbd4ae2SNico                        $markupRefObject = MarkupRef::createLinkFromRef($markupRef);
286*4fbd4ae2SNico                        $scheme = $markupRefObject->getSchemeType();
287*4fbd4ae2SNico                        if ($scheme === MarkupRef::WIKI_URI) {
288*4fbd4ae2SNico                            $markupRef = $markupRefObject->getPath()->toAbsoluteId();
289*4fbd4ae2SNico                        }
290*4fbd4ae2SNico                    } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) {
291*4fbd4ae2SNico                        // no a valid ref
292*4fbd4ae2SNico                    }
29304fd306cSNickeau                    $htmlAttributes->addComponentAttributeValue(self::MARKUP_REF_ATTRIBUTE, $markupRef);
2944cadd4f8SNickeau                }
2954cadd4f8SNickeau
2964cadd4f8SNickeau
2974cadd4f8SNickeau                /**
2984cadd4f8SNickeau                 * Extra HTML attribute
2994cadd4f8SNickeau                 */
300531e725cSNickeau                $callStack = CallStack::createFromHandler($handler);
301531e725cSNickeau                $parent = $callStack->moveToParent();
302007225e5Sgerardnico                $parentName = "";
3034cadd4f8SNickeau                if ($parent !== false) {
304531e725cSNickeau
305531e725cSNickeau                    /**
306531e725cSNickeau                     * Button Link
307531e725cSNickeau                     * Getting the attributes
308531e725cSNickeau                     */
309531e725cSNickeau                    $parentName = $parent->getTagName();
31004fd306cSNickeau                    if ($parentName == ButtonTag::MARKUP_LONG) {
3114cadd4f8SNickeau                        $htmlAttributes->mergeWithCallStackArray($parent->getAttributes());
31221913ab3SNickeau                    }
313531e725cSNickeau
314531e725cSNickeau                    /**
315531e725cSNickeau                     * Searching Clickable parent
316531e725cSNickeau                     */
317531e725cSNickeau                    $maxLevel = 3;
318531e725cSNickeau                    $level = 0;
319531e725cSNickeau                    while (
320531e725cSNickeau                        $parent != false &&
321531e725cSNickeau                        !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) &&
322531e725cSNickeau                        $level < $maxLevel
323531e725cSNickeau                    ) {
324531e725cSNickeau                        $parent = $callStack->moveToParent();
325531e725cSNickeau                        $level++;
3265f891b7eSNickeau                    }
32704fd306cSNickeau                    if ($parent !== false) {
328531e725cSNickeau                        if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) {
32904fd306cSNickeau                            $htmlAttributes->addClassName(self::STRETCHED_LINK);
330531e725cSNickeau                            $parent->addClassName("position-relative");
331531e725cSNickeau                            $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE);
3325f891b7eSNickeau                        }
33321913ab3SNickeau                    }
33421913ab3SNickeau
335531e725cSNickeau                }
3364cadd4f8SNickeau                $returnedArray[PluginUtility::STATE] = $state;
3374cadd4f8SNickeau                $returnedArray[PluginUtility::ATTRIBUTES] = $htmlAttributes->toCallStackArray();
3384cadd4f8SNickeau                $returnedArray[PluginUtility::CONTEXT] = $parentName;
3394cadd4f8SNickeau                return $returnedArray;
3404cadd4f8SNickeau
3415f891b7eSNickeau            case DOKU_LEXER_UNMATCHED:
3425f891b7eSNickeau
34332b85071SNickeau                $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
3445f891b7eSNickeau                /**
34532b85071SNickeau                 * Delete the separator `|` between the ref and the description if any
3465f891b7eSNickeau                 */
3471fa8c418SNickeau                $tag = CallStack::createFromHandler($handler);
3481fa8c418SNickeau                $parent = $tag->moveToParent();
3491fa8c418SNickeau                if ($parent->getTagName() == self::TAG) {
3505f891b7eSNickeau                    if (strpos($match, '|') === 0) {
35132b85071SNickeau                        $data[PluginUtility::PAYLOAD] = substr($match, 1);
3525f891b7eSNickeau                    }
353007225e5Sgerardnico                }
35432b85071SNickeau                return $data;
355007225e5Sgerardnico
3565f891b7eSNickeau            case DOKU_LEXER_EXIT:
35704fd306cSNickeau
358531e725cSNickeau                $callStack = CallStack::createFromHandler($handler);
359531e725cSNickeau                $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
3604cadd4f8SNickeau
3615f891b7eSNickeau                $openingAttributes = $openingTag->getAttributes();
362531e725cSNickeau                $openingPosition = $openingTag->getKey();
3635f891b7eSNickeau
364531e725cSNickeau                $callStack->moveToEnd();
365531e725cSNickeau                $previousCall = $callStack->previous();
366531e725cSNickeau                $previousCallPosition = $previousCall->getKey();
367531e725cSNickeau                $previousCallContent = $previousCall->getCapturedContent();
368531e725cSNickeau
3694cadd4f8SNickeau                /**
3704cadd4f8SNickeau                 * Link label
3714cadd4f8SNickeau                 * is set if there is no content
3724cadd4f8SNickeau                 * between enter and exit node
3734cadd4f8SNickeau                 */
3744cadd4f8SNickeau                $linkLabel = "";
375531e725cSNickeau                if (
376531e725cSNickeau                    $openingPosition == $previousCallPosition // ie [[id]]
377531e725cSNickeau                    ||
378531e725cSNickeau                    ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]]
379531e725cSNickeau                ) {
3805f891b7eSNickeau                    // There is no name
38104fd306cSNickeau                    $markupRef = $openingTag->getAttribute(self::MARKUP_REF_ATTRIBUTE);
38204fd306cSNickeau                    if ($markupRef !== null) {
38304fd306cSNickeau                        try {
38404fd306cSNickeau                            $linkLabel = LinkMarkup::createFromRef($markupRef)
38504fd306cSNickeau                                ->getDefaultLabel();
38604fd306cSNickeau                        } catch (ExceptionCompile $e) {
38704fd306cSNickeau                            LogUtility::error("No default Label can be defined. Error while parsing the markup ref ($markupRef). Error: {$e->getMessage()}", self::CANONICAL, $e);
38804fd306cSNickeau                        }
38904fd306cSNickeau
3904cadd4f8SNickeau                    }
3915f891b7eSNickeau                }
3925f891b7eSNickeau                return array(
3935f891b7eSNickeau                    PluginUtility::STATE => $state,
3945f891b7eSNickeau                    PluginUtility::ATTRIBUTES => $openingAttributes,
3954cadd4f8SNickeau                    PluginUtility::PAYLOAD => $linkLabel,
39685e82846SNickeau                    PluginUtility::CONTEXT => $openingTag->getContext()
3975f891b7eSNickeau                );
3985f891b7eSNickeau        }
3995f891b7eSNickeau        return true;
4005f891b7eSNickeau
401007225e5Sgerardnico
402007225e5Sgerardnico    }
403007225e5Sgerardnico
404007225e5Sgerardnico    /**
405007225e5Sgerardnico     * Render the output
406007225e5Sgerardnico     * @param string $format
407007225e5Sgerardnico     * @param Doku_Renderer $renderer
408007225e5Sgerardnico     * @param array $data - what the function handle() return'ed
409007225e5Sgerardnico     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
410007225e5Sgerardnico     * @see DokuWiki_Syntax_Plugin::render()
411007225e5Sgerardnico     *
412007225e5Sgerardnico     *
413007225e5Sgerardnico     */
414c3437056SNickeau    function render($format, Doku_Renderer $renderer, $data): bool
415007225e5Sgerardnico    {
416007225e5Sgerardnico        // The data
41704fd306cSNickeau        $state = $data[PluginUtility::STATE];
418007225e5Sgerardnico        switch ($format) {
419007225e5Sgerardnico            case 'xhtml':
420007225e5Sgerardnico
421007225e5Sgerardnico                /** @var Doku_Renderer_xhtml $renderer */
422007225e5Sgerardnico                /**
42319b0880dSgerardnico                 * Cache problem may occurs while releasing
424007225e5Sgerardnico                 */
425007225e5Sgerardnico                if (isset($data[PluginUtility::ATTRIBUTES])) {
426531e725cSNickeau                    $callStackAttributes = $data[PluginUtility::ATTRIBUTES];
427007225e5Sgerardnico                } else {
428531e725cSNickeau                    $callStackAttributes = $data;
429007225e5Sgerardnico                }
4305f891b7eSNickeau
43104fd306cSNickeau                PluginUtility::getSnippetManager()->attachCssInternalStyleSheet(self::TAG);
4325f891b7eSNickeau
4335f891b7eSNickeau                switch ($state) {
4345f891b7eSNickeau                    case DOKU_LEXER_ENTER:
435531e725cSNickeau                        $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG);
4364cadd4f8SNickeau
43704fd306cSNickeau                        $markupRef = $tagAttributes->getValueAndRemove(self::MARKUP_REF_ATTRIBUTE);
43804fd306cSNickeau                        if ($markupRef === null) {
43904fd306cSNickeau                            $message = "Internal Error: A link reference was not found";
44004fd306cSNickeau                            LogUtility::internalError($message);
44104fd306cSNickeau                            $renderer->doc .= LogUtility::wrapInRedForHtml($message);
4424cadd4f8SNickeau                            return false;
4434cadd4f8SNickeau                        }
44404fd306cSNickeau                        try {
44504fd306cSNickeau                            $markupRef = syntax_plugin_combo_variable::replaceVariablesWithValuesFromContext($markupRef);
44604fd306cSNickeau                            $markupLink = LinkMarkup::createFromRef($markupRef);
44704fd306cSNickeau                            $markupAttributes = $markupLink->toAttributes();
44804fd306cSNickeau                        } catch (ExceptionCompile $e) {
44904fd306cSNickeau                            // uncomment to get the original error stack trace in dev
45004fd306cSNickeau                            // and see where the exception comes from
45104fd306cSNickeau                            // Don't forget to comment back
45204fd306cSNickeau//                            if (PluginUtility::isDevOrTest()) {
45304fd306cSNickeau//                                throw new ExceptionRuntime("Error on markup ref", self::TAG, 0, $e);
45404fd306cSNickeau//                            }
45504fd306cSNickeau
45604fd306cSNickeau                            /**
45704fd306cSNickeau                             * Error. Example: unknown inter-wiki ...
45804fd306cSNickeau                             * We still create the a to be xhtml compliante
45904fd306cSNickeau                             */
46004fd306cSNickeau                            $url = UrlEndpoint::createSupportUrl();
46104fd306cSNickeau                            $markupAttributes = TagAttributes::createEmpty()
46204fd306cSNickeau                                ->addOutputAttributeValue("href", $url->toString())
46304fd306cSNickeau                                ->addClassName(LinkMarkup::getHtmlClassNotExist());
46404fd306cSNickeau                            $renderer->doc .= $markupAttributes->toHtmlEnterTag("a") . $e->getMessage();
46504fd306cSNickeau
46604fd306cSNickeau                            LogUtility::warning($e->getMessage(), "link", $e);
46704fd306cSNickeau                            return false;
4684cadd4f8SNickeau
4694cadd4f8SNickeau                        }
47004fd306cSNickeau                        // markup attributes is leading because it has already output attribute such as href
47104fd306cSNickeau                        $markupAttributes->mergeWithCallStackArray($tagAttributes->toCallStackArray());
47204fd306cSNickeau                        $tagAttributes = $markupAttributes;
47304fd306cSNickeau
474d262537cSgerardnico
47519b0880dSgerardnico                        /**
4765f891b7eSNickeau                         * Extra styling
47719b0880dSgerardnico                         */
4785f891b7eSNickeau                        $parentTag = $data[PluginUtility::CONTEXT];
4794cadd4f8SNickeau                        $htmlPrefix = "";
4809f4383e9Sgerardnico                        switch ($parentTag) {
48121913ab3SNickeau                            /**
48221913ab3SNickeau                             * Button link
48321913ab3SNickeau                             */
48404fd306cSNickeau                            case ButtonTag::MARKUP_LONG:
4854cadd4f8SNickeau                                $tagAttributes->addOutputAttributeValue("role", "button");
48604fd306cSNickeau                                ButtonTag::processButtonAttributesToHtmlAttributes($tagAttributes);
4879f4383e9Sgerardnico                                break;
48804fd306cSNickeau                            case DropDownTag::TAG:
4894cadd4f8SNickeau                                $tagAttributes->addClassName("dropdown-item");
4904cadd4f8SNickeau                                break;
4914cadd4f8SNickeau                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
4924cadd4f8SNickeau                                $tagAttributes->addClassName("navbar-link");
4934cadd4f8SNickeau                                $htmlPrefix = '<div class="navbar-nav">';
4944cadd4f8SNickeau                                break;
4954cadd4f8SNickeau                            case syntax_plugin_combo_navbargroup::COMPONENT:
4964cadd4f8SNickeau                                $tagAttributes->addClassName("nav-link");
4974cadd4f8SNickeau                                $htmlPrefix = '<li class="nav-item">';
4984cadd4f8SNickeau                                break;
4994cadd4f8SNickeau                            default:
5005f891b7eSNickeau                            case syntax_plugin_combo_badge::TAG:
5019f4383e9Sgerardnico                            case syntax_plugin_combo_cite::TAG:
5029337a630SNickeau                            case syntax_plugin_combo_contentlistitem::DOKU_TAG:
5039f4383e9Sgerardnico                            case syntax_plugin_combo_preformatted::TAG:
5049f4383e9Sgerardnico                                break;
5050a517624Sgerardnico
5069f4383e9Sgerardnico                        }
5079f4383e9Sgerardnico
50819b0880dSgerardnico                        /**
50919b0880dSgerardnico                         * Add it to the rendering
51019b0880dSgerardnico                         */
5114cadd4f8SNickeau                        $renderer->doc .= $htmlPrefix . $tagAttributes->toHtmlEnterTag("a");
5125f891b7eSNickeau                        break;
5135f891b7eSNickeau                    case DOKU_LEXER_UNMATCHED:
51432b85071SNickeau                        $renderer->doc .= PluginUtility::renderUnmatched($data);
5155f891b7eSNickeau                        break;
5165f891b7eSNickeau                    case DOKU_LEXER_EXIT:
5175f891b7eSNickeau
5185f891b7eSNickeau                        // if there is no link name defined, we get the name as ref in the payload
5195f891b7eSNickeau                        // otherwise null string
52070bbd7f1Sgerardnico                        $renderer->doc .= $data[PluginUtility::PAYLOAD] ?? '';
5215f891b7eSNickeau
522e3d0019cSgerardnico                        // Close the link
52385e82846SNickeau                        $renderer->doc .= "</a>";
524e3d0019cSgerardnico
525e3d0019cSgerardnico                        // Close the html wrapper element
5265f891b7eSNickeau                        $context = $data[PluginUtility::CONTEXT];
5275f891b7eSNickeau                        switch ($context) {
5285f891b7eSNickeau                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
5295f891b7eSNickeau                                $renderer->doc .= '</div>';
5305f891b7eSNickeau                                break;
5315f891b7eSNickeau                            case syntax_plugin_combo_navbargroup::COMPONENT:
5325f891b7eSNickeau                                $renderer->doc .= '</li>';
5335f891b7eSNickeau                                break;
5345f891b7eSNickeau                        }
5355f891b7eSNickeau
5365f891b7eSNickeau                }
537007225e5Sgerardnico                return true;
5385f891b7eSNickeau            case 'metadata':
539007225e5Sgerardnico
5404cadd4f8SNickeau                /**
5414cadd4f8SNickeau                 * @var Doku_Renderer_metadata $renderer
5424cadd4f8SNickeau                 */
5434cadd4f8SNickeau                switch ($state) {
5444cadd4f8SNickeau                    case DOKU_LEXER_ENTER:
545007225e5Sgerardnico                        /**
546007225e5Sgerardnico                         * Keep track of the backlinks ie meta['relation']['references']
547007225e5Sgerardnico                         * @var Doku_Renderer_metadata $renderer
548007225e5Sgerardnico                         */
5494cadd4f8SNickeau                        $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
55004fd306cSNickeau
55104fd306cSNickeau                        $markupRef = $tagAttributes->getValue(self::MARKUP_REF_ATTRIBUTE);
55204fd306cSNickeau                        if ($markupRef === null) {
55304fd306cSNickeau                            LogUtility::internalError("The markup ref was not found for a link.");
5544cadd4f8SNickeau                            return false;
555007225e5Sgerardnico                        }
55604fd306cSNickeau                        try {
55704fd306cSNickeau                            $type = MarkupRef::createLinkFromRef($markupRef)
55804fd306cSNickeau                                ->getSchemeType();
55904fd306cSNickeau                        } catch (ExceptionCompile $e) {
56004fd306cSNickeau                            LogUtility::error("The markup ref could not be parsed. Error:{$e->getMessage()}");
56104fd306cSNickeau                            return false;
56204fd306cSNickeau                        }
5634cadd4f8SNickeau                        $name = $tagAttributes->getValue(self::ATTRIBUTE_LABEL);
5649f4383e9Sgerardnico
5654cadd4f8SNickeau                        switch ($type) {
5664cadd4f8SNickeau                            case MarkupRef::WIKI_URI:
5674cadd4f8SNickeau                                /**
5684cadd4f8SNickeau                                 * The relative link should be passed (ie the original)
5694cadd4f8SNickeau                                 * Dokuwiki has a default description
5704cadd4f8SNickeau                                 * We can't pass empty or the array(title), it does not work
5714cadd4f8SNickeau                                 */
5724cadd4f8SNickeau                                $descriptionToDelete = "b";
57304fd306cSNickeau                                $renderer->internallink($markupRef, $descriptionToDelete);
5744cadd4f8SNickeau                                $renderer->doc = substr($renderer->doc, 0, -strlen($descriptionToDelete));
5754cadd4f8SNickeau                                break;
5764cadd4f8SNickeau                            case MarkupRef::WEB_URI:
57704fd306cSNickeau                                $renderer->externallink($markupRef, $name);
5784cadd4f8SNickeau                                break;
5794cadd4f8SNickeau                            case MarkupRef::LOCAL_URI:
58004fd306cSNickeau                                $renderer->locallink($markupRef, $name);
5814cadd4f8SNickeau                                break;
5824cadd4f8SNickeau                            case MarkupRef::EMAIL_URI:
58304fd306cSNickeau                                $renderer->emaillink($markupRef, $name);
5844cadd4f8SNickeau                                break;
5854cadd4f8SNickeau                            case MarkupRef::INTERWIKI_URI:
58604fd306cSNickeau                                $interWikiSplit = preg_split("/>/", $markupRef);
58704fd306cSNickeau                                $renderer->interwikilink($markupRef, $name, $interWikiSplit[0], $interWikiSplit[1]);
5884cadd4f8SNickeau                                break;
5894cadd4f8SNickeau                            case MarkupRef::WINDOWS_SHARE_URI:
59004fd306cSNickeau                                $renderer->windowssharelink($markupRef, $name);
5914cadd4f8SNickeau                                break;
5924cadd4f8SNickeau                            case MarkupRef::VARIABLE_URI:
5934cadd4f8SNickeau                                // No backlinks for link template
5944cadd4f8SNickeau                                break;
5954cadd4f8SNickeau                            default:
59604fd306cSNickeau                                LogUtility::msg("The markup reference ({$markupRef}) with the type $type was not processed into the metadata");
5979f4383e9Sgerardnico                        }
598007225e5Sgerardnico
599007225e5Sgerardnico                        return true;
6004cadd4f8SNickeau                    case DOKU_LEXER_UNMATCHED:
6014cadd4f8SNickeau                        $renderer->doc .= PluginUtility::renderUnmatched($data);
6024cadd4f8SNickeau                        break;
6035f891b7eSNickeau                }
604007225e5Sgerardnico                break;
605007225e5Sgerardnico
606531e725cSNickeau            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
6075f891b7eSNickeau
60804fd306cSNickeau                if ($state === DOKU_LEXER_ENTER) {
609007225e5Sgerardnico                    /**
610007225e5Sgerardnico                     *
611007225e5Sgerardnico                     * @var renderer_plugin_combo_analytics $renderer
612007225e5Sgerardnico                     */
6134cadd4f8SNickeau                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
61404fd306cSNickeau                    $ref = $tagAttributes->getValue(self::MARKUP_REF_ATTRIBUTE);
61504fd306cSNickeau                    try {
61604fd306cSNickeau                        $markupRef = LinkMarkup::createFromRef($ref);
61704fd306cSNickeau                    } catch (ExceptionCompile $e) {
61804fd306cSNickeau                        LogUtility::error("Error while parsing the ref ($ref). Error: {$e->getMessage()}. No further analytics.");
6194cadd4f8SNickeau                        return false;
6204cadd4f8SNickeau                    }
62104fd306cSNickeau                    $refType = $markupRef->getMarkupRef()->getSchemeType();
6224cadd4f8SNickeau
6234cadd4f8SNickeau
6244cadd4f8SNickeau                    /**
6254cadd4f8SNickeau                     * @param array $stats
6264cadd4f8SNickeau                     * Calculate internal link statistics
6274cadd4f8SNickeau                     */
6284cadd4f8SNickeau                    $stats = &$renderer->stats;
6294cadd4f8SNickeau                    switch ($refType) {
6304cadd4f8SNickeau
6314cadd4f8SNickeau                        case MarkupRef::WIKI_URI:
6324cadd4f8SNickeau
6334cadd4f8SNickeau                            /**
6344cadd4f8SNickeau                             * Internal link count
6354cadd4f8SNickeau                             */
63604fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT, $stats)) {
63704fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT] = 0;
6384cadd4f8SNickeau                            }
63904fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT]++;
6404cadd4f8SNickeau
6414cadd4f8SNickeau
6424cadd4f8SNickeau                            /**
6434cadd4f8SNickeau                             * Broken link ?
6444cadd4f8SNickeau                             */
64504fd306cSNickeau                            try {
64604fd306cSNickeau                                $path = $markupRef->getMarkupRef()->getPath();
64704fd306cSNickeau                                $linkedPage = MarkupPath::createPageFromPathObject($path);
64804fd306cSNickeau                                if (!FileSystems::exists($path)) {
64970bbd7f1Sgerardnico                                    $internalLinkBroken = $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT] ?? 0;
65070bbd7f1Sgerardnico                                    $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT] = $internalLinkBroken + 1;
65104fd306cSNickeau                                    $stats[renderer_plugin_combo_analytics::INFO][] = "The internal linked page `{$linkedPage}` does not exist";
65204fd306cSNickeau                                }
65304fd306cSNickeau                            } catch (ExceptionNotFound $e) {
65404fd306cSNickeau                                // no local path
6554cadd4f8SNickeau                            }
6564cadd4f8SNickeau
6574cadd4f8SNickeau                            /**
6584cadd4f8SNickeau                             * Calculate link distance
6594cadd4f8SNickeau                             */
6604cadd4f8SNickeau                            global $ID;
66104fd306cSNickeau                            $id = $linkedPage->getWikiId();
6624cadd4f8SNickeau                            $a = explode(':', getNS($ID));
6634cadd4f8SNickeau                            $b = explode(':', getNS($id));
6644cadd4f8SNickeau                            while (isset($a[0]) && $a[0] == $b[0]) {
6654cadd4f8SNickeau                                array_shift($a);
6664cadd4f8SNickeau                                array_shift($b);
6674cadd4f8SNickeau                            }
6684cadd4f8SNickeau                            $length = count($a) + count($b);
66904fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_DISTANCE][] = $length;
6704cadd4f8SNickeau                            break;
6714cadd4f8SNickeau
6724cadd4f8SNickeau                        case MarkupRef::WEB_URI:
6734cadd4f8SNickeau
67404fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT, $stats)) {
67504fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT] = 0;
6764cadd4f8SNickeau                            }
67704fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT]++;
6784cadd4f8SNickeau                            break;
6794cadd4f8SNickeau
6804cadd4f8SNickeau                        case MarkupRef::LOCAL_URI:
6814cadd4f8SNickeau
68204fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::LOCAL_LINK_COUNT, $stats)) {
68304fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::LOCAL_LINK_COUNT] = 0;
6844cadd4f8SNickeau                            }
68504fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::LOCAL_LINK_COUNT]++;
6864cadd4f8SNickeau                            break;
6874cadd4f8SNickeau
6884cadd4f8SNickeau                        case MarkupRef::INTERWIKI_URI:
6894cadd4f8SNickeau
69004fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT, $stats)) {
69104fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT] = 0;
6924cadd4f8SNickeau                            }
69304fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT]++;
6944cadd4f8SNickeau                            break;
6954cadd4f8SNickeau
6964cadd4f8SNickeau                        case MarkupRef::EMAIL_URI:
6974cadd4f8SNickeau
69804fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::EMAIL_COUNT, $stats)) {
69904fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::EMAIL_COUNT] = 0;
7004cadd4f8SNickeau                            }
70104fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::EMAIL_COUNT]++;
7024cadd4f8SNickeau                            break;
7034cadd4f8SNickeau
7044cadd4f8SNickeau                        case MarkupRef::WINDOWS_SHARE_URI:
7054cadd4f8SNickeau
70604fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT, $stats)) {
70704fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT] = 0;
7084cadd4f8SNickeau                            }
70904fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT]++;
7104cadd4f8SNickeau                            break;
7114cadd4f8SNickeau
7124cadd4f8SNickeau                        case MarkupRef::VARIABLE_URI:
7134cadd4f8SNickeau
71404fd306cSNickeau                            if (!array_key_exists(renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT, $stats)) {
71504fd306cSNickeau                                $stats[renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT] = 0;
7164cadd4f8SNickeau                            }
71704fd306cSNickeau                            $stats[renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT]++;
7184cadd4f8SNickeau                            break;
7194cadd4f8SNickeau
7204cadd4f8SNickeau                        default:
7214cadd4f8SNickeau
7224cadd4f8SNickeau                            LogUtility::msg("The link `{$ref}` with the type ($refType)  is not taken into account into the statistics");
7234cadd4f8SNickeau
7244cadd4f8SNickeau                    }
7254cadd4f8SNickeau
7264cadd4f8SNickeau
727007225e5Sgerardnico                    break;
7285f891b7eSNickeau                }
729007225e5Sgerardnico
730007225e5Sgerardnico        }
73104fd306cSNickeau
732007225e5Sgerardnico        return false;
733007225e5Sgerardnico    }
734007225e5Sgerardnico
735007225e5Sgerardnico
7364cadd4f8SNickeau    /**
7374cadd4f8SNickeau     * Utility function to add a link into the callstack
7384cadd4f8SNickeau     * @param CallStack $callStack
7394cadd4f8SNickeau     * @param TagAttributes $tagAttributes
7404cadd4f8SNickeau     */
74104fd306cSNickeau    public
74204fd306cSNickeau    static function addOpenLinkTagInCallStack(CallStack $callStack, TagAttributes $tagAttributes)
7434cadd4f8SNickeau    {
7444cadd4f8SNickeau        $parent = $callStack->moveToParent();
7454cadd4f8SNickeau        $context = "";
7464cadd4f8SNickeau        $attributes = $tagAttributes->toCallStackArray();
7474cadd4f8SNickeau        if ($parent !== false) {
7484cadd4f8SNickeau            $context = $parent->getTagName();
74904fd306cSNickeau            if ($context === ButtonTag::MARKUP_LONG) {
7504cadd4f8SNickeau                // the link takes by default the data from the button
7514cadd4f8SNickeau                $parentAttributes = $parent->getAttributes();
7524cadd4f8SNickeau                if ($parentAttributes !== null) {
7534cadd4f8SNickeau                    $attributes = ArrayUtility::mergeByValue($parentAttributes, $attributes);
7544cadd4f8SNickeau                }
7554cadd4f8SNickeau            }
7564cadd4f8SNickeau        }
7574cadd4f8SNickeau        $callStack->appendCallAtTheEnd(
7584cadd4f8SNickeau            Call::createComboCall(
7594cadd4f8SNickeau                syntax_plugin_combo_link::TAG,
7604cadd4f8SNickeau                DOKU_LEXER_ENTER,
7614cadd4f8SNickeau                $attributes,
7624cadd4f8SNickeau                $context
7634cadd4f8SNickeau            ));
7644cadd4f8SNickeau    }
7654cadd4f8SNickeau
76604fd306cSNickeau    public
76704fd306cSNickeau    static function addExitLinkTagInCallStack(CallStack $callStack)
7684cadd4f8SNickeau    {
7694cadd4f8SNickeau        $callStack->appendCallAtTheEnd(
7704cadd4f8SNickeau            Call::createComboCall(
7714cadd4f8SNickeau                syntax_plugin_combo_link::TAG,
7724cadd4f8SNickeau                DOKU_LEXER_EXIT
7734cadd4f8SNickeau            ));
7744cadd4f8SNickeau    }
775007225e5Sgerardnico}
776007225e5Sgerardnico
777