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