1<?php
2
3
4require_once(__DIR__ . "/../ComboStrap/PluginUtility.php");
5
6use ComboStrap\CallStack;
7use ComboStrap\LinkUtility;
8use ComboStrap\PluginUtility;
9use ComboStrap\TagAttributes;
10use ComboStrap\ThirdPartyPlugins;
11
12if (!defined('DOKU_INC')) die();
13
14/**
15 *
16 * A link pattern to take over the link of Dokuwiki
17 * and transform it as a bootstrap link
18 *
19 * The handle of the move of link is to be found in the
20 * admin action {@link action_plugin_combo_linkmove}
21 *
22 */
23class syntax_plugin_combo_link extends DokuWiki_Syntax_Plugin
24{
25    const TAG = 'link';
26    const COMPONENT = 'combo_link';
27
28    /**
29     * Disable the link component
30     */
31    const CONF_DISABLE_LINK = "disableLink";
32
33    /**
34     * The link Tag
35     * a or p
36     */
37    const LINK_TAG = "linkTag";
38
39    /**
40     * Do the link component allows to be spawn on multilines
41     */
42    const CLICKABLE_ATTRIBUTE = "clickable";
43
44
45    /**
46     * Syntax Type.
47     *
48     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
49     * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types
50     */
51    function getType()
52    {
53        return 'substition';
54    }
55
56    /**
57     * How Dokuwiki will add P element
58     *
59     *  * 'normal' - The plugin can be used inside paragraphs
60     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
61     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
62     *
63     * @see DokuWiki_Syntax_Plugin::getPType()
64     */
65    function getPType()
66    {
67        return 'normal';
68    }
69
70    /**
71     * @return array
72     * Allow which kind of plugin inside
73     *
74     * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
75     * because we manage self the content and we call self the parser
76     */
77    function getAllowedTypes()
78    {
79        return array('substition', 'formatting', 'disabled');
80    }
81
82    /**
83     * @param string $mode
84     * @return bool
85     * Accepts inside
86     */
87    public function accepts($mode)
88    {
89        /**
90         * To avoid that the description if it contains a link
91         * will be taken by the links mode
92         *
93         * For instance, [[https://hallo|https://hallo]] will send https://hallo
94         * to the external link mode
95         */
96        $linkModes = [
97            "externallink",
98            "locallink",
99            "internallink",
100            "interwikilink",
101            "emaillink",
102            "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description
103            //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet
104            //"emphasis_close",
105            //"acrnonym"
106        ];
107        if (in_array($mode, $linkModes)) {
108            return false;
109        } else {
110            return true;
111        }
112    }
113
114
115    /**
116     * @see Doku_Parser_Mode::getSort()
117     * The mode with the lowest sort number will win out
118     */
119    function getSort()
120    {
121        /**
122         * It should be less than the number
123         * at {@link \dokuwiki\Parsing\ParserMode\Internallink::getSort}
124         * and the like
125         *
126         * For whatever reason, the number below should be less than 100,
127         * otherwise on windows with DokuWiki Stick, the link syntax may be not taken
128         * into account
129         */
130        return 99;
131    }
132
133
134    function connectTo($mode)
135    {
136
137        if (!$this->getConf(self::CONF_DISABLE_LINK, false)
138            &&
139            $mode !== PluginUtility::getModeFromPluginName(ThirdPartyPlugins::IMAGE_MAPPING_NAME)
140        ) {
141
142            $pattern = LinkUtility::ENTRY_PATTERN_SINGLE_LINE;
143            $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
144
145        }
146
147    }
148
149    public function postConnect()
150    {
151        if (!$this->getConf(self::CONF_DISABLE_LINK, false)) {
152            $this->Lexer->addExitPattern(LinkUtility::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent()));
153        }
154    }
155
156
157    /**
158     * The handler for an internal link
159     * based on `internallink` in {@link Doku_Handler}
160     * The handler call the good renderer in {@link Doku_Renderer_xhtml} with
161     * the parameters (ie for instance internallink)
162     * @param string $match
163     * @param int $state
164     * @param int $pos
165     * @param Doku_Handler $handler
166     * @return array|bool
167     */
168    function handle($match, $state, $pos, Doku_Handler $handler)
169    {
170
171
172        switch ($state) {
173            case DOKU_LEXER_ENTER:
174                $tagAttributes = TagAttributes::createFromCallStackArray(LinkUtility::parse($match));
175                $callStack = CallStack::createFromHandler($handler);
176
177
178                $parent = $callStack->moveToParent();
179                $parentName = "";
180                if ($parent != false) {
181
182                    /**
183                     * Button Link
184                     * Getting the attributes
185                     */
186                    $parentName = $parent->getTagName();
187                    if ($parentName == syntax_plugin_combo_button::TAG) {
188                        $tagAttributes->mergeWithCallStackArray($parent->getAttributes());
189                    }
190
191                    /**
192                     * Searching Clickable parent
193                     */
194                    $maxLevel = 3;
195                    $level = 0;
196                    while (
197                        $parent != false &&
198                        !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) &&
199                        $level < $maxLevel
200                    ) {
201                        $parent = $callStack->moveToParent();
202                        $level++;
203                    }
204                    if ($parent != false) {
205                        if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) {
206                            $tagAttributes->addClassName("stretched-link");
207                            $parent->addClassName("position-relative");
208                            $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE);
209                        }
210                    }
211
212                }
213                $callStackAttributes = $tagAttributes->toCallStackArray();
214                return array(
215                    PluginUtility::STATE => $state,
216                    PluginUtility::ATTRIBUTES => $callStackAttributes,
217                    PluginUtility::CONTEXT => $parentName
218                );
219            case DOKU_LEXER_UNMATCHED:
220
221                $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
222                /**
223                 * Delete the separator `|` between the ref and the description if any
224                 */
225                $tag = CallStack::createFromHandler($handler);
226                $parent = $tag->moveToParent();
227                if ($parent->getTagName() == self::TAG) {
228                    if (strpos($match, '|') === 0) {
229                        $data[PluginUtility::PAYLOAD] = substr($match, 1);
230                    }
231                }
232                return $data;
233
234            case DOKU_LEXER_EXIT:
235                $callStack = CallStack::createFromHandler($handler);
236                $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
237                $openingAttributes = $openingTag->getAttributes();
238                $openingPosition = $openingTag->getKey();
239
240                $callStack->moveToEnd();
241                $previousCall = $callStack->previous();
242                $previousCallPosition = $previousCall->getKey();
243                $previousCallContent = $previousCall->getCapturedContent();
244
245                if (
246                    $openingPosition == $previousCallPosition // ie [[id]]
247                    ||
248                    ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]]
249                ) {
250                    // There is no name
251                    $link = new LinkUtility($openingAttributes[LinkUtility::ATTRIBUTE_REF]);
252                    $linkName = $link->getName();
253                } else {
254                    $linkName = "";
255                }
256                return array(
257                    PluginUtility::STATE => $state,
258                    PluginUtility::ATTRIBUTES => $openingAttributes,
259                    PluginUtility::PAYLOAD => $linkName,
260                    PluginUtility::CONTEXT => $openingTag->getContext()
261                );
262        }
263        return true;
264
265
266    }
267
268    /**
269     * Render the output
270     * @param string $format
271     * @param Doku_Renderer $renderer
272     * @param array $data - what the function handle() return'ed
273     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
274     * @see DokuWiki_Syntax_Plugin::render()
275     *
276     *
277     */
278    function render($format, Doku_Renderer $renderer, $data): bool
279    {
280        // The data
281        switch ($format) {
282            case 'xhtml':
283
284                /** @var Doku_Renderer_xhtml $renderer */
285                /**
286                 * Cache problem may occurs while releasing
287                 */
288                if (isset($data[PluginUtility::ATTRIBUTES])) {
289                    $callStackAttributes = $data[PluginUtility::ATTRIBUTES];
290                } else {
291                    $callStackAttributes = $data;
292                }
293
294                PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG);
295
296                $state = $data[PluginUtility::STATE];
297                switch ($state) {
298                    case DOKU_LEXER_ENTER:
299                        $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG);
300                        $ref = $tagAttributes->getValueAndRemove(LinkUtility::ATTRIBUTE_REF);
301                        $link = LinkUtility::createFromRef($ref, $tagAttributes);
302
303                        /**
304                         * Extra styling
305                         */
306                        $parentTag = $data[PluginUtility::CONTEXT];
307                        switch ($parentTag) {
308                            /**
309                             * Button link
310                             */
311                            case syntax_plugin_combo_button::TAG:
312                                $tagAttributes->addHtmlAttributeValue("role", "button");
313                                syntax_plugin_combo_button::processButtonAttributesToHtmlAttributes($tagAttributes);
314                                $htmlLink = $link->renderOpenTag($renderer);
315                                break;
316                            case syntax_plugin_combo_badge::TAG:
317                            case syntax_plugin_combo_cite::TAG:
318                            case syntax_plugin_combo_contentlistitem::DOKU_TAG:
319                            case syntax_plugin_combo_preformatted::TAG:
320                                $htmlLink = $link->renderOpenTag($renderer);
321                                break;
322                            case syntax_plugin_combo_dropdown::TAG:
323                                $tagAttributes->addClassName("dropdown-item");
324                                $htmlLink = $link->renderOpenTag($renderer);
325                                break;
326                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
327                                $tagAttributes->addClassName("navbar-link");
328                                $htmlLink = '<div class="navbar-nav">' . $link->renderOpenTag($renderer);
329                                break;
330                            case syntax_plugin_combo_navbargroup::COMPONENT:
331                                $tagAttributes->addClassName("nav-link");
332                                $htmlLink = '<li class="nav-item">' . $link->renderOpenTag($renderer);
333                                break;
334                            default:
335                                $htmlLink = $link->renderOpenTag($renderer);
336
337                        }
338
339
340                        /**
341                         * Add it to the rendering
342                         */
343                        $renderer->doc .= $htmlLink;
344                        break;
345                    case DOKU_LEXER_UNMATCHED:
346                        $renderer->doc .= PluginUtility::renderUnmatched($data);
347                        break;
348                    case DOKU_LEXER_EXIT:
349
350                        // if there is no link name defined, we get the name as ref in the payload
351                        // otherwise null string
352                        $renderer->doc .= $data[PluginUtility::PAYLOAD];
353
354                        // Close the link
355                        $renderer->doc .= "</a>";
356
357                        // Close the html wrapper element
358                        $context = $data[PluginUtility::CONTEXT];
359                        switch ($context) {
360                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
361                                $renderer->doc .= '</div>';
362                                break;
363                            case syntax_plugin_combo_navbargroup::COMPONENT:
364                                $renderer->doc .= '</li>';
365                                break;
366                        }
367
368
369                }
370
371
372                return true;
373
374            case 'metadata':
375
376                $state = $data[PluginUtility::STATE];
377                if ($state == DOKU_LEXER_ENTER) {
378                    /**
379                     * Keep track of the backlinks ie meta['relation']['references']
380                     * @var Doku_Renderer_metadata $renderer
381                     */
382                    if (isset($data[PluginUtility::ATTRIBUTES])) {
383                        $tagAttributes = $data[PluginUtility::ATTRIBUTES];
384                    } else {
385                        $tagAttributes = $data;
386                    }
387                    $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF];
388
389                    $link = new LinkUtility($ref);
390                    $name = $tagAttributes[LinkUtility::ATTRIBUTE_NAME];
391                    if ($name != null) {
392                        $link->setName($name);
393                    }
394                    $link->handleMetadata($renderer);
395
396                    return true;
397                }
398                break;
399
400            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
401
402                $state = $data[PluginUtility::STATE];
403                if ($state == DOKU_LEXER_ENTER) {
404                    /**
405                     *
406                     * @var renderer_plugin_combo_analytics $renderer
407                     */
408                    $tagAttributes = $data[PluginUtility::ATTRIBUTES];
409                    $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF];
410                    $link = new LinkUtility($ref);
411                    $link->processLinkStats($renderer->stats);
412                    break;
413                }
414
415        }
416        // unsupported $mode
417        return false;
418    }
419
420
421}
422
423