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