xref: /plugin/combo/syntax/link.php (revision 531e725cdb5a652164f2d97f556304e31f720033)
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/HtmlUtility.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 CONF_ENABLE_MULTI_LINES_LINK = "enableMultiLinesLink";
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    public function accepts($mode)
87    {
88        /**
89         * To avoid that the description if it contains a link
90         * will be taken by the links mode
91         *
92         * For instance, [[https://hallo|https://hallo]] will send https://hallo
93         * to the external link mode
94         */
95        $linkModes = [
96            "externallink",
97            "locallink",
98            "internallink",
99            "interwikilink",
100            "emaillink",
101            "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description
102            //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet
103            //"emphasis_close",
104            //"acrnonym"
105        ];
106        if (in_array($mode, $linkModes)) {
107            return false;
108        } else {
109            return true;
110        }
111    }
112
113
114    /**
115     * @see Doku_Parser_Mode::getSort()
116     * The mode with the lowest sort number will win out
117     */
118    function getSort()
119    {
120        return 100;
121    }
122
123
124    function connectTo($mode)
125    {
126
127        if (!$this->getConf(self::CONF_DISABLE_LINK, false)) {
128            $pattern = LinkUtility::ENTRY_PATTERN_SINGLE_LINE;
129            if ($this->getConf(self::CONF_ENABLE_MULTI_LINES_LINK, false)) {
130                $pattern = LinkUtility::ENTRY_PATTERN_MULTI_LINE;
131            }
132            $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeForComponent($this->getPluginComponent()));
133        }
134
135    }
136
137    public function postConnect()
138    {
139        if (!$this->getConf(self::CONF_DISABLE_LINK, false)) {
140            $this->Lexer->addExitPattern(LinkUtility::EXIT_PATTERN, PluginUtility::getModeForComponent($this->getPluginComponent()));
141        }
142    }
143
144
145    /**
146     * The handler for an internal link
147     * based on `internallink` in {@link Doku_Handler}
148     * The handler call the good renderer in {@link Doku_Renderer_xhtml} with
149     * the parameters (ie for instance internallink)
150     * @param string $match
151     * @param int $state
152     * @param int $pos
153     * @param Doku_Handler $handler
154     * @return array|bool
155     */
156    function handle($match, $state, $pos, Doku_Handler $handler)
157    {
158
159
160        switch ($state) {
161            case DOKU_LEXER_ENTER:
162                $tagAttributes = TagAttributes::createFromCallStackArray(LinkUtility::parse($match));
163                $callStack = CallStack::createFromHandler($handler);
164
165
166                $parent = $callStack->moveToParent();
167                $parentName = "";
168                if ($parent != false) {
169
170                    /**
171                     * Button Link
172                     * Getting the attributes
173                     */
174                    $parentName = $parent->getTagName();
175                    if ($parentName == syntax_plugin_combo_button::TAG) {
176                        $tagAttributes->mergeWithCallStackArray($parent->getAttributes());
177                    }
178
179                    /**
180                     * Searching Clickable parent
181                     */
182                    $maxLevel = 3;
183                    $level = 0;
184                    while (
185                        $parent != false &&
186                        !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) &&
187                        $level < $maxLevel
188                    ) {
189                        $parent = $callStack->moveToParent();
190                        $level++;
191                    }
192                    if ($parent != false) {
193                        if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) {
194                            $tagAttributes->addClassName("stretched-link");
195                            $parent->addClassName("position-relative");
196                            $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE);
197                        }
198                    }
199
200                }
201
202                $link = new LinkUtility($tagAttributes->getValue(LinkUtility::ATTRIBUTE_REF));
203                $linkTag = $link->getHtmlTag();
204                return array(
205                    PluginUtility::STATE => $state,
206                    PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(),
207                    PluginUtility::CONTEXT => $parentName,
208                    self::LINK_TAG => $linkTag
209                );
210            case DOKU_LEXER_UNMATCHED:
211
212                $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
213                /**
214                 * Delete the separator `|` between the ref and the description if any
215                 */
216                $tag = new Tag(self::TAG, array(), $state, $handler);
217                $parent = $tag->getParent();
218                if ($parent->getName() == self::TAG) {
219                    if (strpos($match, '|') === 0) {
220                        $data[PluginUtility::PAYLOAD] = substr($match, 1);
221                    }
222                }
223                return $data;
224
225            case DOKU_LEXER_EXIT:
226                $callStack = CallStack::createFromHandler($handler);
227                $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
228                $openingAttributes = $openingTag->getAttributes();
229                $linkTag = $openingTag->getPluginData()[self::LINK_TAG];
230                $openingPosition = $openingTag->getKey();
231
232                $callStack->moveToEnd();
233                $previousCall = $callStack->previous();
234                $previousCallPosition = $previousCall->getKey();
235                $previousCallContent = $previousCall->getCapturedContent();
236
237                if (
238                    $openingPosition == $previousCallPosition // ie [[id]]
239                    ||
240                    ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]]
241                ) {
242                    // There is no name
243                    $link = new LinkUtility($openingAttributes[LinkUtility::ATTRIBUTE_REF]);
244                    $linkName = $link->getName();
245                } else {
246                    $linkName = "";
247                }
248                return array(
249                    PluginUtility::STATE => $state,
250                    PluginUtility::ATTRIBUTES => $openingAttributes,
251                    PluginUtility::PAYLOAD => $linkName,
252                    PluginUtility::CONTEXT => $openingTag->getContext(),
253                    self::LINK_TAG => $linkTag
254                );
255        }
256        return true;
257
258
259    }
260
261    /**
262     * Render the output
263     * @param string $format
264     * @param Doku_Renderer $renderer
265     * @param array $data - what the function handle() return'ed
266     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
267     * @see DokuWiki_Syntax_Plugin::render()
268     *
269     *
270     */
271    function render($format, Doku_Renderer $renderer, $data)
272    {
273        // The data
274        switch ($format) {
275            case 'xhtml':
276
277                /** @var Doku_Renderer_xhtml $renderer */
278                /**
279                 * Cache problem may occurs while releasing
280                 */
281                if (isset($data[PluginUtility::ATTRIBUTES])) {
282                    $callStackAttributes = $data[PluginUtility::ATTRIBUTES];
283                } else {
284                    $callStackAttributes = $data;
285                }
286
287                PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG);
288
289                $state = $data[PluginUtility::STATE];
290                switch ($state) {
291                    case DOKU_LEXER_ENTER:
292                        $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG);
293                        $ref = $tagAttributes->getValueAndRemove(LinkUtility::ATTRIBUTE_REF);
294                        $link = new LinkUtility($ref, $tagAttributes);
295
296                        /**
297                         * Extra styling
298                         */
299                        $parentTag = $data[PluginUtility::CONTEXT];
300                        switch ($parentTag) {
301                            /**
302                             * Button link
303                             */
304                            case syntax_plugin_combo_button::TAG:
305                                $tagAttributes->addHtmlAttributeValue("role", "button");
306                                syntax_plugin_combo_button::processButtonAttributesToHtmlAttributes($tagAttributes);
307                                $htmlLink = $link->renderOpenTag($renderer);
308                                break;
309                            case syntax_plugin_combo_badge::TAG:
310                            case syntax_plugin_combo_cite::TAG:
311                            case syntax_plugin_combo_listitem::TAG:
312                            case syntax_plugin_combo_preformatted::TAG:
313                                $htmlLink = $link->renderOpenTag($renderer);
314                                break;
315                            case syntax_plugin_combo_dropdown::TAG:
316                                $tagAttributes->addClassName("dropdown-item");
317                                $htmlLink = $link->renderOpenTag($renderer);
318                                break;
319                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
320                                $tagAttributes->addClassName("navbar-link");
321                                $htmlLink = '<div class="navbar-nav">' . $link->renderOpenTag($renderer);
322                                break;
323                            case syntax_plugin_combo_navbargroup::COMPONENT:
324                                $tagAttributes->addClassName("nav-link");
325                                $htmlLink = '<li class="nav-item">' . $link->renderOpenTag($renderer);
326                                break;
327                            default:
328                                $htmlLink = $link->renderOpenTag($renderer);
329
330                        }
331
332
333                        /**
334                         * Add it to the rendering
335                         */
336                        $renderer->doc .= $htmlLink;
337                        break;
338                    case DOKU_LEXER_UNMATCHED:
339                        $renderer->doc .= PluginUtility::renderUnmatched($data);
340                        break;
341                    case DOKU_LEXER_EXIT:
342
343                        // if there is no link name defined, we get the name as ref in the payload
344                        // otherwise null string
345                        $renderer->doc .= $data[PluginUtility::PAYLOAD];;
346
347                        // Close the link
348                        $linkTag = $data[self::LINK_TAG];
349                        $renderer->doc .= "</$linkTag>";
350
351                        // Close the html wrapper element
352                        $context = $data[PluginUtility::CONTEXT];
353                        switch ($context) {
354                            case syntax_plugin_combo_navbarcollapse::COMPONENT:
355                                $renderer->doc .= '</div>';
356                                break;
357                            case syntax_plugin_combo_navbargroup::COMPONENT:
358                                $renderer->doc .= '</li>';
359                                break;
360                        }
361
362
363                }
364
365
366                return true;
367                break;
368
369            case 'metadata':
370
371                $state = $data[PluginUtility::STATE];
372                if ($state == DOKU_LEXER_ENTER) {
373                    /**
374                     * Keep track of the backlinks ie meta['relation']['references']
375                     * @var Doku_Renderer_metadata $renderer
376                     */
377                    if (isset($data[PluginUtility::ATTRIBUTES])) {
378                        $tagAttributes = $data[PluginUtility::ATTRIBUTES];
379                    } else {
380                        $tagAttributes = $data;
381                    }
382                    $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF];
383
384                    $link = new LinkUtility($ref);
385                    $name = $tagAttributes[LinkUtility::ATTRIBUTE_NAME];
386                    if ($name != null) {
387                        $link->setName($name);
388                    }
389                    $link->handleMetadata($renderer);
390
391                    return true;
392                }
393                break;
394
395            case renderer_plugin_combo_analytics::RENDERER_FORMAT:
396
397                $state = $data[PluginUtility::STATE];
398                if ($state == DOKU_LEXER_ENTER) {
399                    /**
400                     *
401                     * @var renderer_plugin_combo_analytics $renderer
402                     */
403                    $tagAttributes = $data[PluginUtility::ATTRIBUTES];
404                    $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF];
405                    $link = new LinkUtility($ref);
406                    $link->processLinkStats($renderer->stats);
407                    break;
408                }
409
410        }
411        // unsupported $mode
412        return false;
413    }
414
415
416}
417
418