1<?php
2
3
4// must be run within Dokuwiki
5use ComboStrap\Brand;
6use ComboStrap\BrandButton;
7use ComboStrap\CacheDependencies;
8use ComboStrap\CacheManager;
9use ComboStrap\Call;
10use ComboStrap\CallStack;
11use ComboStrap\ColorRgb;
12use ComboStrap\Dimension;
13use ComboStrap\ExceptionCombo;
14use ComboStrap\Icon;
15use ComboStrap\LogUtility;
16use ComboStrap\Page;
17use ComboStrap\PluginUtility;
18use ComboStrap\TagAttributes;
19use ComboStrap\Template;
20use ComboStrap\TemplateUtility;
21
22if (!defined('DOKU_INC')) die();
23
24
25class syntax_plugin_combo_brand extends DokuWiki_Syntax_Plugin
26{
27
28    const TAG = "brand";
29    const CANONICAL = self::TAG;
30
31
32    public const ICON_ATTRIBUTE = "icon";
33
34    public const URL_ATTRIBUTE = "url";
35
36    /**
37     * Class needed
38     * https://getbootstrap.com/docs/5.1/components/navbar/#image-and-text
39     */
40    const BOOTSTRAP_NAV_BAR_IMAGE_AND_TEXT_CLASS = "d-inline-block align-text-top";
41
42    const WIDGET_ATTRIBUTE = "widget";
43
44    const BRAND_IMAGE_FOUND_INDICATOR = "brand_image_found";
45    const BRAND_TEXT_FOUND_INDICATOR = "brand_text_found";
46
47
48    public static function addOpenLinkTagInCallStack(CallStack $callStack, TagAttributes $tagAttributes)
49    {
50        $linkArrayAttributes = $tagAttributes->toCallStackArray();
51        $linkArrayAttributes[TagAttributes::TYPE_KEY] = $tagAttributes->getLogicalTag();
52        $linkAttributes = TagAttributes::createFromCallStackArray($linkArrayAttributes);
53        syntax_plugin_combo_link::addOpenLinkTagInCallStack($callStack, $linkAttributes);
54    }
55
56    /**
57     * @throws ExceptionCombo
58     */
59    public static function mixBrandButtonToTagAttributes(TagAttributes $tagAttributes, BrandButton $brandButton)
60    {
61        $brandLinkAttributes = $brandButton->getLinkAttributes();
62        $urlAttribute = syntax_plugin_combo_brand::URL_ATTRIBUTE;
63        $url = $tagAttributes->getValueAndRemoveIfPresent($urlAttribute);
64        if ($url !== null) {
65            $urlTemplate = Template::create($url);
66            $variableDetected = $urlTemplate->getVariablesDetected();
67            if (sizeof($variableDetected) === 1 && $variableDetected[0] === "path") {
68                CacheManager::getOrCreate()->addDependencyForCurrentSlot(CacheDependencies::REQUESTED_PAGE_DEPENDENCY);
69                $page = Page::createPageFromRequestedPage();
70                $relativePath = str_replace(":", "/", $page->getDokuwikiId());
71                $url = $urlTemplate
72                    ->set("path", $relativePath)
73                    ->render();
74            }
75            $tagAttributes->addOutputAttributeValue("href", $url);
76        }
77        $tagAttributes->mergeWithCallStackArray($brandLinkAttributes->toCallStackArray());
78    }
79
80
81    /**
82     * An utility constructor to be sure that we build the brand button
83     * with the same data in the handle and render function
84     * @throws ExceptionCombo
85     */
86    public static function createButtonFromAttributes(TagAttributes $brandAttributes, $type = BrandButton::TYPE_BUTTON_BRAND): BrandButton
87    {
88        $brandName = $brandAttributes->getValue(TagAttributes::TYPE_KEY, Brand::CURRENT_BRAND);
89        $widget = $brandAttributes->getValue(self::WIDGET_ATTRIBUTE, BrandButton::WIDGET_BUTTON_VALUE);
90        $icon = $brandAttributes->getValue(self::ICON_ATTRIBUTE, BrandButton::ICON_SOLID_VALUE);
91
92        $brandButton = (new BrandButton($brandName, $type))
93            ->setWidget($widget)
94            ->setIconType($icon);
95
96        $width = $brandAttributes->getValueAsInteger(Dimension::WIDTH_KEY);
97        if ($width !== null) {
98            $brandButton->setWidth($width);
99        }
100        $title = $brandAttributes->getValue(syntax_plugin_combo_link::TITLE_ATTRIBUTE);
101        if ($title !== null) {
102            $brandButton->setLinkTitle($title);
103        }
104        $color = $brandAttributes->getValue(ColorRgb::PRIMARY_VALUE);
105        if ($color !== null) {
106            $brandButton->setPrimaryColor($color);
107        }
108        $secondaryColor = $brandAttributes->getValue(ColorRgb::SECONDARY_VALUE);
109        if ($secondaryColor !== null) {
110            $brandButton->setSecondaryColor($secondaryColor);
111        }
112        $handle = $brandAttributes->getValue(syntax_plugin_combo_follow::HANDLE_ATTRIBUTE);
113        if ($handle !== null) {
114            $brandButton->setHandle($handle);
115        }
116        return $brandButton;
117    }
118
119    /**
120     * Syntax Type.
121     *
122     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
123     * @see DokuWiki_Syntax_Plugin::getType()
124     */
125    function getType(): string
126    {
127        return 'substition';
128    }
129
130    /**
131     * How Dokuwiki will add P element
132     *
133     *  * 'normal' - The plugin can be used inside paragraphs
134     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
135     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
136     *
137     * @see DokuWiki_Syntax_Plugin::getPType()
138     */
139    function getPType(): string
140    {
141        return 'normal';
142    }
143
144    /**
145     * @return array
146     * Allow which kind of plugin inside
147     *
148     * array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
149     *
150     */
151    function getAllowedTypes(): array
152    {
153        return array('baseonly', 'formatting', 'substition', 'protected', 'disabled');
154    }
155
156    function getSort(): int
157    {
158        return 201;
159    }
160
161    public
162    function accepts($mode): bool
163    {
164        return syntax_plugin_combo_preformatted::disablePreformatted($mode);
165    }
166
167
168    /**
169     * Create a pattern that will called this plugin
170     *
171     * @param string $mode
172     * @see Doku_Parser_Mode::connectTo()
173     */
174    function connectTo($mode)
175    {
176
177        $pattern = PluginUtility::getContainerTagPattern(self::getTag());
178        $this->Lexer->addEntryPattern($pattern, $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
179
180        /**
181         * The empty tag pattern should be after the container pattern
182         */
183        $this->Lexer->addSpecialPattern(PluginUtility::getEmptyTagPattern(self::TAG), $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
184
185    }
186
187    function postConnect()
188    {
189
190        $this->Lexer->addExitPattern('</' . self::getTag() . '>', 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
191
192    }
193
194    function handle($match, $state, $pos, Doku_Handler $handler)
195    {
196
197
198        switch ($state) {
199
200            case DOKU_LEXER_SPECIAL :
201            case DOKU_LEXER_ENTER :
202
203                /**
204                 * Context
205                 */
206                $callStack = CallStack::createFromHandler($handler);
207                $parent = $callStack->moveToParent();
208                $context = null;
209                if ($parent !== false) {
210                    $context = $parent->getTagName();
211                }
212
213                /**
214                 * Default parameters, type definition and parsing
215                 */
216                if ($context === syntax_plugin_combo_menubar::TAG) {
217                    $defaultWidget = BrandButton::WIDGET_LINK_VALUE;
218                } else {
219                    $defaultWidget = BrandButton::WIDGET_BUTTON_VALUE;
220                }
221                $defaultParameters[TagAttributes::TYPE_KEY] = Brand::CURRENT_BRAND;
222                $defaultParameters[self::WIDGET_ATTRIBUTE] = $defaultWidget;
223                $knownTypes = null;
224                $tagAttributes = TagAttributes::createFromTagMatch($match, $defaultParameters, $knownTypes)
225                    ->setLogicalTag(self::TAG);
226
227
228                return array(
229                    PluginUtility::STATE => $state,
230                    PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(),
231                    PluginUtility::CONTEXT => $context
232                );
233
234            case DOKU_LEXER_UNMATCHED :
235                return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
236
237            case DOKU_LEXER_EXIT :
238
239                $callStack = CallStack::createFromHandler($handler);
240                $openTag = $callStack->moveToPreviousCorrespondingOpeningCall();
241                $openTagAttributes = TagAttributes::createFromCallStackArray($openTag->getAttributes());
242                $openTagContext = $openTag->getContext();
243                /**
244                 * Old syntax
245                 * An icon/image could be already inside
246                 * We go from end to start to
247                 * see if there is also a text, if this is the case,
248                 * there is a class added on the media
249                 */
250                $markupIconImageFound = false;
251                $textFound = false;
252                $callStack->moveToEnd();
253                while ($actualCall = $callStack->previous()) {
254                    $tagName = $actualCall->getTagName();
255                    if (in_array($tagName, [syntax_plugin_combo_icon::TAG, syntax_plugin_combo_media::TAG])) {
256
257
258                        if ($textFound && $openTagContext === syntax_plugin_combo_menubar::TAG) {
259                            // if text and icon
260                            // We add it here because, if they are present, we don't add them later
261                            // for all on raster image
262                            $actualCall->addClassName(self::BOOTSTRAP_NAV_BAR_IMAGE_AND_TEXT_CLASS);
263                        }
264
265                        // is it a added call / no content
266                        // or is it an icon from the markup
267                        if ($actualCall->getCapturedContent() === null) {
268
269                            // It's an added call
270                            // No user icon, image can be found anymore
271                            // exiting
272                            break;
273                        }
274
275                        $primary = $openTagAttributes->getValue(ColorRgb::PRIMARY_VALUE);
276                        if ($primary !== null && $tagName === syntax_plugin_combo_icon::TAG) {
277                            try {
278                                $brandButton = self::createButtonFromAttributes($openTagAttributes);
279                                $actualCall->addAttribute(ColorRgb::COLOR, $brandButton->getTextColor());
280                            } catch (ExceptionCombo $e) {
281                                LogUtility::msg("Error while trying to set the icon color on exit. Error: {$e->getMessage()}");
282                            }
283                        }
284
285                        $markupIconImageFound = true;
286                    }
287                    if ($actualCall->getState() === DOKU_LEXER_UNMATCHED) {
288                        $textFound = true;
289                    }
290                }
291                $openTag->setPluginData(self::BRAND_IMAGE_FOUND_INDICATOR, $markupIconImageFound);
292                $openTag->setPluginData(self::BRAND_TEXT_FOUND_INDICATOR, $textFound);
293
294                return array(
295                    PluginUtility::STATE => $state
296                );
297
298
299        }
300        return array();
301
302    }
303
304    /**
305     * Render the output
306     * @param string $format
307     * @param Doku_Renderer $renderer
308     * @param array $data - what the function handle() return
309     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
310     * @see DokuWiki_Syntax_Plugin::render()
311     *
312     *
313     */
314    function render($format, Doku_Renderer $renderer, $data): bool
315    {
316
317        if ($format === "xhtml") {
318            $state = $data[PluginUtility::STATE];
319            switch ($state) {
320                case DOKU_LEXER_SPECIAL:
321                case DOKU_LEXER_ENTER:
322
323                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
324                    /**
325                     * Brand Object creation
326                     */
327                    $brandName = $tagAttributes->getType();
328                    try {
329                        $brandButton = self::createButtonFromAttributes($tagAttributes);
330                    } catch (ExceptionCombo $e) {
331                        $renderer->doc .= LogUtility::wrapInRedForHtml("Error while reading the brand data for the brand ($brandName). Error: {$e->getMessage()}");
332                        return false;
333                    }
334                    /**
335                     * Link
336                     */
337                    try {
338                        self::mixBrandButtonToTagAttributes($tagAttributes, $brandButton);
339                    } catch (ExceptionCombo $e) {
340                        $renderer->doc .= LogUtility::wrapInRedForHtml("Error while getting the link data for the the brand ($brandName). Error: {$e->getMessage()}");
341                        return false;
342                    }
343                    $context = $data[PluginUtility::CONTEXT];
344                    if ($context === syntax_plugin_combo_menubar::TAG) {
345                        $tagAttributes->addOutputAttributeValue("accesskey", "h");
346                        $tagAttributes->addClassName("navbar-brand");
347                    }
348                    // Width does not apply to link (otherwise the link got a max-width of 30)
349                    $tagAttributes->removeComponentAttributeIfPresent(Dimension::WIDTH_KEY);
350                    // Widget also
351                    $tagAttributes->removeComponentAttributeIfPresent(self::WIDGET_ATTRIBUTE);
352                    $renderer->doc .= $tagAttributes
353                        ->setType(self::CANONICAL)
354                        ->setLogicalTag(syntax_plugin_combo_link::TAG)
355                        ->toHtmlEnterTag("a");
356
357
358                    /**
359                     * Logo
360                     */
361                    $brandImageFound = $data[self::BRAND_IMAGE_FOUND_INDICATOR];
362                    if (!$brandImageFound && $brandButton->hasIcon()) {
363                        try {
364                            $iconAttributes = $brandButton->getIconAttributes();
365                            $textFound = $data[self::BRAND_TEXT_FOUND_INDICATOR];
366                            $name = $iconAttributes[\syntax_plugin_combo_icon::ICON_NAME_ATTRIBUTE];
367                            $iconAttributes = TagAttributes::createFromCallStackArray($iconAttributes);
368                            if ($textFound && $context === syntax_plugin_combo_menubar::TAG) {
369                                $iconAttributes->addClassName(self::BOOTSTRAP_NAV_BAR_IMAGE_AND_TEXT_CLASS);
370                            }
371                            $renderer->doc .= Icon::create($name, $iconAttributes)
372                                ->render();
373                        } catch (ExceptionCombo $e) {
374
375                            if ($brandButton->getBrand()->getName() === Brand::CURRENT_BRAND) {
376
377                                $documentationLink = PluginUtility::getDocumentationHyperLink("logo", "documentation");
378                                LogUtility::msg("A svg logo icon is not installed on your website. Check the corresponding $documentationLink.", LogUtility::LVL_MSG_INFO);
379
380                            } else {
381
382                                $renderer->doc .= "The brand icon returns an error. Error: {$e->getMessage()}";
383                                // we don't return because the link is not closed
384
385                            }
386
387                        }
388                    }
389
390                    /**
391                     * End of link
392                     */
393                    if ($state === DOKU_LEXER_SPECIAL) {
394                        $renderer->doc .= "</a>";
395                    }
396
397                    /**
398                     * Add the Icon / CSS / Javascript snippet
399                     *
400                     */
401                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
402                    try {
403                        $brandButton = self::createButtonFromAttributes($tagAttributes);
404                    } catch (ExceptionCombo $e) {
405                        LogUtility::msg("The brand could not be build. Error: {$e->getMessage()}");
406                        return false;
407                    }
408                    try {
409                        $style = $brandButton->getStyle();
410                    } catch (ExceptionCombo $e) {
411                        LogUtility::msg("The style of the {$this->getType()} button ($brandButton) could not be determined. Error: {$e->getMessage()}");
412                        return false;
413                    }
414                    $snippetId = $brandButton->getStyleScriptIdentifier();
415                    PluginUtility::getSnippetManager()->attachCssInternalStyleSheetForSlot($snippetId, $style);
416                    break;
417                case DOKU_LEXER_UNMATCHED:
418                    $renderer->doc .= PluginUtility::renderUnmatched($data);
419                    break;
420                case DOKU_LEXER_EXIT:
421                    $renderer->doc .= "</a>";
422                    break;
423
424            }
425            return true;
426        }
427
428        // unsupported $mode
429        return false;
430    }
431
432    public
433    static function getTag(): string
434    {
435        return self::TAG;
436    }
437
438    /**
439     *
440     * @throws ExceptionCombo
441     */
442    public
443    static function addIconInCallStack(CallStack $callStack, BrandButton $brandButton)
444    {
445
446        if (!$brandButton->hasIcon()) {
447            return;
448        }
449        $iconAttributes = $brandButton->getIconAttributes();
450
451        $callStack->appendCallAtTheEnd(
452            Call::createComboCall(
453                syntax_plugin_combo_icon::TAG,
454                DOKU_LEXER_SPECIAL,
455                $iconAttributes
456            ));
457    }
458
459}
460
461