1<?php
2
3
4require_once(__DIR__ . "/../ComboStrap/PluginUtility.php");
5
6use ComboStrap\AnalyticsDocument;
7use ComboStrap\ArrayUtility;
8use ComboStrap\Call;
9use ComboStrap\CallStack;
10use ComboStrap\ExceptionCombo;
11use ComboStrap\MarkupRef;
12use ComboStrap\LogUtility;
13use ComboStrap\Page;
14use ComboStrap\PluginUtility;
15use ComboStrap\TagAttributes;
16use ComboStrap\ThirdPartyPlugins;
17
18if (!defined('DOKU_INC')) die();
19
20/**
21 *
22 * A link pattern to take over the link of Dokuwiki
23 * and transform it as a bootstrap link
24 *
25 * The handle of the move of link is to be found in the
26 * admin action {@link action_plugin_combo_linkmove}
27 *
28 */
29class syntax_plugin_combo_link extends DokuWiki_Syntax_Plugin
30{
31    const TAG = 'link';
32    const COMPONENT = 'combo_link';
33
34    /**
35     * Disable the link component
36     */
37    const CONF_DISABLE_LINK = "disableLink";
38
39    /**
40     * The link Tag
41     * a or p
42     */
43    const LINK_TAG = "linkTag";
44
45    /**
46     * Do the link component allows to be spawn on multilines
47     */
48    const CLICKABLE_ATTRIBUTE = "clickable";
49    public const ATTRIBUTE_LABEL = 'label';
50    /**
51     * The key of the array for the handle cache
52     */
53    public const ATTRIBUTE_HREF = 'href';
54    /**
55     * Indicate if the href is a {@link MarkupRef}
56     * (ie the syntax from the markup document)
57     * or is a html href added by {@link syntax_plugin_combo_share}
58     * for instance
59     */
60    const ATTRIBUTE_HREF_TYPE = "href-type";
61    const HREF_MARKUP_TYPE_VALUE = "markup";
62    public const ATTRIBUTE_IMAGE_IN_LABEL = 'image-in-label';
63
64    /**
65     * A link may have a title or not
66     * ie
67     * [[path:page]]
68     * [[path:page|title]]
69     * are valid
70     *
71     * Get the content until one of this character is found:
72     *   * |
73     *   * or ]]
74     *   * or \n (No line break allowed, too much difficult to debug)
75     *   * and not [ (for two links on the same line)
76     */
77    public const ENTRY_PATTERN_SINGLE_LINE = "\[\[[^\|\]]*(?=[^\n\[]*\]\])";
78    public const EXIT_PATTERN = "\]\]";
79
80
81    /**
82     * Dokuwiki Link pattern ter info
83     * Found in {@link \dokuwiki\Parsing\ParserMode\Internallink}
84     */
85    const SPECIAL_PATTERN = "\[\[.*?\]\](?!\])";
86
87    /**
88     * The link title attribute (ie popup)
89     */
90    const TITLE_ATTRIBUTE = "title";
91
92
93    /**
94     * Parse the match of a syntax {@link DokuWiki_Syntax_Plugin} handle function
95     * @param $match
96     * @return string[] - an array with the attributes constant `ATTRIBUTE_xxxx` as key
97     *
98     * Code adapted from  {@link Doku_Handler::internallink()}
99     */
100    public static function parse($match): array
101    {
102
103        // Strip the opening and closing markup
104        $linkString = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match);
105
106        // Split title from URL
107        $linkArray = explode('|', $linkString, 2);
108
109        // Id
110        $attributes[self::ATTRIBUTE_HREF] = trim($linkArray[0]);
111
112
113        // Text or image
114        if (!isset($linkArray[1])) {
115            $attributes[self::ATTRIBUTE_LABEL] = null;
116        } else {
117            // An image in the title
118            if (preg_match('/^\{\{[^\}]+\}\}$/', $linkArray[1])) {
119                // If the title is an image, convert it to an array containing the image details
120                $attributes[self::ATTRIBUTE_IMAGE_IN_LABEL] = Doku_Handler_Parse_Media($linkArray[1]);
121            } else {
122                $attributes[self::ATTRIBUTE_LABEL] = $linkArray[1];
123            }
124        }
125
126        return $attributes;
127
128    }
129
130
131    /**
132     * Syntax Type.
133     *
134     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
135     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
136     */
137    function getType()
138    {
139        return 'substition';
140    }
141
142    /**
143     * How Dokuwiki will add P element
144     *
145     *  * 'normal' - The plugin can be used inside paragraphs
146     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
147     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
148     *
149     * @see DokuWiki_Syntax_Plugin::getPType()
150     */
151    function getPType()
152    {
153        return 'normal';
154    }
155
156    /**
157     * @return array
158     * Allow which kind of plugin inside
159     *
160     * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
161     * because we manage self the content and we call self the parser
162     */
163    function getAllowedTypes(): array
164    {
165        return array('substition', 'formatting', 'disabled');
166    }
167
168    /**
169     * @param string $mode
170     * @return bool
171     * Accepts inside
172     */
173    public function accepts($mode): bool
174    {
175        /**
176         * To avoid that the description if it contains a link
177         * will be taken by the links mode
178         *
179         * For instance, [[https://hallo|https://hallo]] will send https://hallo
180         * to the external link mode
181         */
182        $linkModes = [
183            "externallink",
184            "locallink",
185            "internallink",
186            "interwikilink",
187            "emaillink",
188            "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description
189            //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet
190            //"emphasis_close",
191            //"acrnonym"
192        ];
193        if (in_array($mode, $linkModes)) {
194            return false;
195        } else {
196            return true;
197        }
198    }
199
200
201    /**
202     * @see Doku_Parser_Mode::getSort()
203     * The mode with the lowest sort number will win out
204     */
205    function getSort()
206    {
207        /**
208         * It should be less than the number
209         * at {@link \dokuwiki\Parsing\ParserMode\Internallink::getSort}
210         * and the like
211         *
212         * For whatever reason, the number below should be less than 100,
213         * otherwise on windows with DokuWiki Stick, the link syntax may be not taken
214         * into account
215         */
216        return 99;
217    }
218
219
220    function connectTo($mode)
221    {
222
223        if (!$this->getConf(self::CONF_DISABLE_LINK, false)
224            &&
225            $mode !== PluginUtility::getModeFromPluginName(ThirdPartyPlugins::IMAGE_MAPPING_NAME)
226        ) {
227
228            $pattern = self::ENTRY_PATTERN_SINGLE_LINE;
229            $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
230
231        }
232
233    }
234
235    public function postConnect()
236    {
237        if (!$this->getConf(self::CONF_DISABLE_LINK, false)) {
238            $this->Lexer->addExitPattern(self::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent()));
239        }
240    }
241
242
243    /**
244     * The handler for an internal link
245     * based on `internallink` in {@link Doku_Handler}
246     * The handler call the good renderer in {@link Doku_Renderer_xhtml} with
247     * the parameters (ie for instance internallink)
248     * @param string $match
249     * @param int $state
250     * @param int $pos
251     * @param Doku_Handler $handler
252     * @return array|bool
253     */
254    function handle($match, $state, $pos, Doku_Handler $handler)
255    {
256
257        switch ($state) {
258            case DOKU_LEXER_ENTER:
259                $parsedArray = self::parse($match);
260                $htmlAttributes = TagAttributes::createEmpty(self::TAG);
261                /**
262                 * Href needs to be passed to the
263                 * instructions stack (because we support)
264                 * dynamic link call href with {@link syntax_plugin_combo_template}
265                 */
266                $href = $parsedArray[self::ATTRIBUTE_HREF];
267                if ($href !== null) {
268                    $htmlAttributes
269                        ->addComponentAttributeValue(self::ATTRIBUTE_HREF, $href)
270                        ->addComponentAttributeValue(self::ATTRIBUTE_HREF_TYPE, self::HREF_MARKUP_TYPE_VALUE);
271                }
272
273
274                /**
275                 * Extra HTML attribute
276                 */
277                $callStack = CallStack::createFromHandler($handler);
278                $parent = $callStack->moveToParent();
279                $parentName = "";
280                if ($parent !== false) {
281
282                    /**
283                     * Button Link
284                     * Getting the attributes
285                     */
286                    $parentName = $parent->getTagName();
287                    if ($parentName == syntax_plugin_combo_button::TAG) {
288                        $htmlAttributes->mergeWithCallStackArray($parent->getAttributes());
289                    }
290
291                    /**
292                     * Searching Clickable parent
293                     */
294                    $maxLevel = 3;
295                    $level = 0;
296                    while (
297                        $parent != false &&
298                        !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) &&
299                        $level < $maxLevel
300                    ) {
301                        $parent = $callStack->moveToParent();
302                        $level++;
303                    }
304                    if ($parent != false) {
305                        if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) {
306                            $htmlAttributes->addClassName("stretched-link");
307                            $parent->addClassName("position-relative");
308                            $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE);
309                        }
310                    }
311
312                }
313                $returnedArray[PluginUtility::STATE] = $state;
314                $returnedArray[PluginUtility::ATTRIBUTES] = $htmlAttributes->toCallStackArray();
315                $returnedArray[PluginUtility::CONTEXT] = $parentName;
316                return $returnedArray;
317
318            case DOKU_LEXER_UNMATCHED:
319
320                $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
321                /**
322                 * Delete the separator `|` between the ref and the description if any
323                 */
324                $tag = CallStack::createFromHandler($handler);
325                $parent = $tag->moveToParent();
326                if ($parent->getTagName() == self::TAG) {
327                    if (strpos($match, '|') === 0) {
328                        $data[PluginUtility::PAYLOAD] = substr($match, 1);
329                    }
330                }
331                return $data;
332
333            case DOKU_LEXER_EXIT:
334                $callStack = CallStack::createFromHandler($handler);
335                $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
336
337                $openingAttributes = $openingTag->getAttributes();
338                $openingPosition = $openingTag->getKey();
339
340                $callStack->moveToEnd();
341                $previousCall = $callStack->previous();
342                $previousCallPosition = $previousCall->getKey();
343                $previousCallContent = $previousCall->getCapturedContent();
344
345                /**
346                 * Link label
347                 * is set if there is no content
348                 * between enter and exit node
349                 */
350                $linkLabel = "";
351                if (
352                    $openingPosition == $previousCallPosition // ie [[id]]
353                    ||
354                    ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]]
355                ) {
356                    // There is no name
357                    $href = $openingTag->getAttribute(self::ATTRIBUTE_HREF);
358                    if ($href !== null) {
359                        $markup = MarkupRef::createFromRef($href);
360                        $linkLabel = $markup->getLabel();
361                    }
362                }
363                return array(
364                    PluginUtility::STATE => $state,
365                    PluginUtility::ATTRIBUTES => $openingAttributes,
366                    PluginUtility::PAYLOAD => $linkLabel,
367                    PluginUtility::CONTEXT => $openingTag->getContext()
368                );
369        }
370        return true;
371
372
373    }
374
375    /**
376     * Render the output
377     * @param string $format
378     * @param Doku_Renderer $renderer
379     * @param array $data - what the function handle() return'ed
380     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
381     * @see DokuWiki_Syntax_Plugin::render()
382     *
383     *
384     */
385    function render($format, Doku_Renderer $renderer, $data): bool
386    {
387        // The data
388        switch ($format) {
389            case 'xhtml':
390
391                /** @var Doku_Renderer_xhtml $renderer */
392                /**
393                 * Cache problem may occurs while releasing
394                 */
395                if (isset($data[PluginUtility::ATTRIBUTES])) {
396                    $callStackAttributes = $data[PluginUtility::ATTRIBUTES];
397                } else {
398                    $callStackAttributes = $data;
399                }
400
401                PluginUtility::getSnippetManager()->attachCssInternalStyleSheetForSlot(self::TAG);
402
403                $state = $data[PluginUtility::STATE];
404                switch ($state) {
405                    case DOKU_LEXER_ENTER:
406                        $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG);
407
408                        $href = $tagAttributes->getValue(self::ATTRIBUTE_HREF);
409
410                        /**
411                         * HrefMarkup ?
412                         */
413                        $hrefSource = $tagAttributes->getValueAndRemoveIfPresent(self::ATTRIBUTE_HREF_TYPE);
414                        if ($hrefSource !== null) {
415                            try {
416                                $markupRef = MarkupRef::createFromRef($href);
417                                $url = $markupRef->getUrl();
418                                $markupRefAttributes = $markupRef->toAttributes();
419                            } catch (ExceptionCombo $e) {
420                                $message = "Error while parsing the markup href ($href). Error: {$e->getMessage()}";
421                                $renderer->doc .= "<a>." . LogUtility::wrapInRedForHtml($message);
422                                return false;
423                            }
424                            $tagAttributes->mergeWithCallStackArray($markupRefAttributes->toCallStackArray());
425                            // No href if the url could not be calculated
426                            // such as a bad interwiki link
427                            if (!empty($url)) {
428                                $tagAttributes->setComponentAttributeValue(self::ATTRIBUTE_HREF, $url);
429                            } else {
430                                $tagAttributes->removeComponentAttributeIfPresent(self::ATTRIBUTE_HREF);
431                            }
432
433                        }
434
435                        /**
436                         * Extra styling
437                         */
438                        $parentTag = $data[PluginUtility::CONTEXT];
439                        $htmlPrefix = "";
440                        switch ($parentTag) {
441                            /**
442                             * Button link
443                             */
444                            case syntax_plugin_combo_button::TAG:
445                                $tagAttributes->addOutputAttributeValue("role", "button");
446                                syntax_plugin_combo_button::processButtonAttributesToHtmlAttributes($tagAttributes);
447                                break;
448                            case syntax_plugin_combo_dropdown::TAG:
449                                $tagAttributes->addClassName("dropdown-item");
450                                break;
451                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
452                                $tagAttributes->addClassName("navbar-link");
453                                $htmlPrefix = '<div class="navbar-nav">';
454                                break;
455                            case syntax_plugin_combo_navbargroup::COMPONENT:
456                                $tagAttributes->addClassName("nav-link");
457                                $htmlPrefix = '<li class="nav-item">';
458                                break;
459                            default:
460                            case syntax_plugin_combo_badge::TAG:
461                            case syntax_plugin_combo_cite::TAG:
462                            case syntax_plugin_combo_contentlistitem::DOKU_TAG:
463                            case syntax_plugin_combo_preformatted::TAG:
464                                break;
465
466                        }
467
468                        /**
469                         * Add it to the rendering
470                         */
471                        $renderer->doc .= $htmlPrefix . $tagAttributes->toHtmlEnterTag("a");
472                        break;
473                    case DOKU_LEXER_UNMATCHED:
474                        $renderer->doc .= PluginUtility::renderUnmatched($data);
475                        break;
476                    case DOKU_LEXER_EXIT:
477
478                        // if there is no link name defined, we get the name as ref in the payload
479                        // otherwise null string
480                        $renderer->doc .= $data[PluginUtility::PAYLOAD];
481
482                        // Close the link
483                        $renderer->doc .= "</a>";
484
485                        // Close the html wrapper element
486                        $context = $data[PluginUtility::CONTEXT];
487                        switch ($context) {
488                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
489                                $renderer->doc .= '</div>';
490                                break;
491                            case syntax_plugin_combo_navbargroup::COMPONENT:
492                                $renderer->doc .= '</li>';
493                                break;
494                        }
495
496
497                }
498
499
500                return true;
501
502            case 'metadata':
503
504                /**
505                 * @var Doku_Renderer_metadata $renderer
506                 */
507                $state = $data[PluginUtility::STATE];
508                switch ($state) {
509                    case DOKU_LEXER_ENTER:
510                        /**
511                         * Keep track of the backlinks ie meta['relation']['references']
512                         * @var Doku_Renderer_metadata $renderer
513                         */
514                        $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
515                        $hrefSource = $tagAttributes->getValue(self::ATTRIBUTE_HREF_TYPE);
516                        if ($hrefSource === null || $hrefSource !== self::HREF_MARKUP_TYPE_VALUE) {
517                            /**
518                             * This is not a markup link
519                             * (ie an external link created by a plugin {@link syntax_plugin_combo_share})
520                             */
521                            return false;
522                        }
523                        $href = $tagAttributes->getValue(self::ATTRIBUTE_HREF);
524                        $type = MarkupRef::createFromRef($href)
525                            ->getUriType();
526                        $name = $tagAttributes->getValue(self::ATTRIBUTE_LABEL);
527
528                        switch ($type) {
529                            case MarkupRef::WIKI_URI:
530                                /**
531                                 * The relative link should be passed (ie the original)
532                                 * Dokuwiki has a default description
533                                 * We can't pass empty or the array(title), it does not work
534                                 */
535                                $descriptionToDelete = "b";
536                                $renderer->internallink($href, $descriptionToDelete);
537                                $renderer->doc = substr($renderer->doc,0,-strlen($descriptionToDelete));
538                                break;
539                            case MarkupRef::WEB_URI:
540                                $renderer->externallink($href, $name);
541                                break;
542                            case MarkupRef::LOCAL_URI:
543                                $renderer->locallink($href, $name);
544                                break;
545                            case MarkupRef::EMAIL_URI:
546                                $renderer->emaillink($href, $name);
547                                break;
548                            case MarkupRef::INTERWIKI_URI:
549                                $interWikiSplit = preg_split("/>/", $href);
550                                $renderer->interwikilink($href, $name, $interWikiSplit[0], $interWikiSplit[1]);
551                                break;
552                            case MarkupRef::WINDOWS_SHARE_URI:
553                                $renderer->windowssharelink($href, $name);
554                                break;
555                            case MarkupRef::VARIABLE_URI:
556                                // No backlinks for link template
557                                break;
558                            default:
559                                LogUtility::msg("The markup reference ({$href}) with the type $type was not processed into the metadata");
560                        }
561
562                        return true;
563                    case DOKU_LEXER_UNMATCHED:
564                        $renderer->doc .= PluginUtility::renderUnmatched($data);
565                        break;
566                }
567                break;
568
569            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
570
571                $state = $data[PluginUtility::STATE];
572                if ($state == DOKU_LEXER_ENTER) {
573                    /**
574                     *
575                     * @var renderer_plugin_combo_analytics $renderer
576                     */
577                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
578                    $refSource = $tagAttributes->getValue(self::ATTRIBUTE_HREF_TYPE);
579                    if ($refSource === null || $refSource !== self::HREF_MARKUP_TYPE_VALUE) {
580                        /**
581                         * Link added programmatically
582                         */
583                        return false;
584                    }
585                    $ref = $tagAttributes->getValue(self::ATTRIBUTE_HREF);
586                    $href = MarkupRef::createFromRef($ref);
587                    $refType = $href->getUriType();
588
589
590                    /**
591                     * @param array $stats
592                     * Calculate internal link statistics
593                     */
594
595                    $stats = &$renderer->stats;
596                    switch ($refType) {
597
598                        case MarkupRef::WIKI_URI:
599
600                            /**
601                             * Internal link count
602                             */
603                            if (!array_key_exists(AnalyticsDocument::INTERNAL_LINK_COUNT, $stats)) {
604                                $stats[AnalyticsDocument::INTERNAL_LINK_COUNT] = 0;
605                            }
606                            $stats[AnalyticsDocument::INTERNAL_LINK_COUNT]++;
607
608
609                            /**
610                             * Broken link ?
611                             */
612
613                            $linkedPage = $href->getInternalPage();
614                            if (!$linkedPage->exists()) {
615                                $stats[AnalyticsDocument::INTERNAL_LINK_BROKEN_COUNT]++;
616                                $stats[AnalyticsDocument::INFO][] = "The internal linked page `{$href->getInternalPage()}` does not exist";
617                            }
618
619                            /**
620                             * Calculate link distance
621                             */
622                            global $ID;
623                            $id = $href->getInternalPage()->getDokuwikiId();
624                            $a = explode(':', getNS($ID));
625                            $b = explode(':', getNS($id));
626                            while (isset($a[0]) && $a[0] == $b[0]) {
627                                array_shift($a);
628                                array_shift($b);
629                            }
630                            $length = count($a) + count($b);
631                            $stats[AnalyticsDocument::INTERNAL_LINK_DISTANCE][] = $length;
632                            break;
633
634                        case MarkupRef::WEB_URI:
635
636                            if (!array_key_exists(AnalyticsDocument::EXTERNAL_LINK_COUNT, $stats)) {
637                                $stats[AnalyticsDocument::EXTERNAL_LINK_COUNT] = 0;
638                            }
639                            $stats[AnalyticsDocument::EXTERNAL_LINK_COUNT]++;
640                            break;
641
642                        case MarkupRef::LOCAL_URI:
643
644                            if (!array_key_exists(AnalyticsDocument::LOCAL_LINK_COUNT, $stats)) {
645                                $stats[AnalyticsDocument::LOCAL_LINK_COUNT] = 0;
646                            }
647                            $stats[AnalyticsDocument::LOCAL_LINK_COUNT]++;
648                            break;
649
650                        case MarkupRef::INTERWIKI_URI:
651
652                            if (!array_key_exists(AnalyticsDocument::INTERWIKI_LINK_COUNT, $stats)) {
653                                $stats[AnalyticsDocument::INTERWIKI_LINK_COUNT] = 0;
654                            }
655                            $stats[AnalyticsDocument::INTERWIKI_LINK_COUNT]++;
656                            break;
657
658                        case MarkupRef::EMAIL_URI:
659
660                            if (!array_key_exists(AnalyticsDocument::EMAIL_COUNT, $stats)) {
661                                $stats[AnalyticsDocument::EMAIL_COUNT] = 0;
662                            }
663                            $stats[AnalyticsDocument::EMAIL_COUNT]++;
664                            break;
665
666                        case MarkupRef::WINDOWS_SHARE_URI:
667
668                            if (!array_key_exists(AnalyticsDocument::WINDOWS_SHARE_COUNT, $stats)) {
669                                $stats[AnalyticsDocument::WINDOWS_SHARE_COUNT] = 0;
670                            }
671                            $stats[AnalyticsDocument::WINDOWS_SHARE_COUNT]++;
672                            break;
673
674                        case MarkupRef::VARIABLE_URI:
675
676                            if (!array_key_exists(AnalyticsDocument::TEMPLATE_LINK_COUNT, $stats)) {
677                                $stats[AnalyticsDocument::TEMPLATE_LINK_COUNT] = 0;
678                            }
679                            $stats[AnalyticsDocument::TEMPLATE_LINK_COUNT]++;
680                            break;
681
682                        default:
683
684                            LogUtility::msg("The link `{$ref}` with the type ($refType)  is not taken into account into the statistics");
685
686                    }
687
688
689                    break;
690                }
691
692        }
693        // unsupported $mode
694        return false;
695    }
696
697
698    /**
699     * Utility function to add a link into the callstack
700     * @param CallStack $callStack
701     * @param TagAttributes $tagAttributes
702     */
703    public static function addOpenLinkTagInCallStack(CallStack $callStack, TagAttributes $tagAttributes)
704    {
705        $parent = $callStack->moveToParent();
706        $context = "";
707        $attributes = $tagAttributes->toCallStackArray();
708        if ($parent !== false) {
709            $context = $parent->getTagName();
710            if ($context === syntax_plugin_combo_button::TAG) {
711                // the link takes by default the data from the button
712                $parentAttributes = $parent->getAttributes();
713                if ($parentAttributes !== null) {
714                    $attributes = ArrayUtility::mergeByValue($parentAttributes, $attributes);
715                }
716            }
717        }
718        $callStack->appendCallAtTheEnd(
719            Call::createComboCall(
720                syntax_plugin_combo_link::TAG,
721                DOKU_LEXER_ENTER,
722                $attributes,
723                $context
724            ));
725    }
726
727    public static function addExitLinkTagInCallStack(CallStack $callStack)
728    {
729        $callStack->appendCallAtTheEnd(
730            Call::createComboCall(
731                syntax_plugin_combo_link::TAG,
732                DOKU_LEXER_EXIT
733            ));
734    }
735}
736
737