1<?php
2/**
3 * DokuWiki Syntax Plugin Combostrap.
4 *
5 */
6
7use ComboStrap\CallStack;
8use ComboStrap\ColorRgb;
9use ComboStrap\Dimension;
10use ComboStrap\DokuPath;
11use ComboStrap\ExceptionCombo;
12use ComboStrap\FileSystems;
13use ComboStrap\Icon;
14use ComboStrap\LogUtility;
15use ComboStrap\PluginUtility;
16use ComboStrap\Site;
17use ComboStrap\SvgDocument;
18use ComboStrap\TagAttributes;
19
20
21require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
22
23/**
24 * All DokuWiki plugins to extend the parser/rendering mechanism
25 * need to inherit from this class
26 *
27 * The name of the class must follow a pattern (don't change it)
28 * ie:
29 *    syntax_plugin_PluginName_ComponentName
30 *
31 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
32 * !!!!!!!!!!! The component name must be the name of the php file !!!
33 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
34 *
35 * https://icons.getbootstrap.com/
36 * https://remixicon.com/
37 */
38class syntax_plugin_combo_icon extends DokuWiki_Syntax_Plugin
39{
40    const TAG = "icon";
41    const CANONICAL = self::TAG;
42    const ICON_NAME_ATTRIBUTE = "name";
43
44    private static function exceptionHandling(Exception $e, $tagAttribute): string
45    {
46        $errorClass = syntax_plugin_combo_media::SVG_RENDERING_ERROR_CLASS;
47        $message = "Icon ({$tagAttribute->getValue("name")}). Error while rendering: {$e->getMessage()}";
48        $html = "<span class=\"text-alert $errorClass\">" . hsc(trim($message)) . "</span>";
49        if (!PluginUtility::isTest()) {
50            LogUtility::msg($message, LogUtility::LVL_MSG_WARNING, self::CANONICAL);
51        }
52        return $html;
53    }
54
55
56    /**
57     * Syntax Type.
58     *
59     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
60     * @see DokuWiki_Syntax_Plugin::getType()
61     */
62    function getType()
63    {
64        return 'substition';
65    }
66
67    /**
68     * @return array
69     * Allow which kind of plugin inside
70     *
71     * No one of array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
72     * because we manage self the content and we call self the parser
73     */
74    public function getAllowedTypes()
75    {
76        // You can't put anything in a icon
77        return array('formatting');
78    }
79
80    /**
81     * How Dokuwiki will add P element
82     *
83     *  * 'normal' - The plugin can be used inside paragraphs
84     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
85     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
86     *
87     * @see DokuWiki_Syntax_Plugin::getPType()
88     */
89    function getPType()
90    {
91        return 'normal';
92    }
93
94    /**
95     * @see Doku_Parser_Mode::getSort()
96     * the mode with the lowest sort number will win out
97     * the lowest in the tree must have the lowest sort number
98     * No idea why it must be low but inside a teaser, it will work
99     * https://www.dokuwiki.org/devel:parser#order_of_adding_modes_important
100     */
101    function getSort()
102    {
103        return 10;
104    }
105
106    /**
107     * Create a pattern that will called this plugin
108     *
109     * @param string $mode
110     * @see Doku_Parser_Mode::connectTo()
111     */
112    function connectTo($mode)
113    {
114
115
116        $specialPattern = PluginUtility::getEmptyTagPattern(self::TAG);
117        $this->Lexer->addSpecialPattern($specialPattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
118
119        /**
120         * The content is used to add a {@link syntax_plugin_combo_tooltip}
121         */
122        $entryPattern = PluginUtility::getContainerTagPattern(self::TAG);
123        $this->Lexer->addEntryPattern($entryPattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
124
125
126    }
127
128    public function postConnect()
129    {
130        $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent()));
131    }
132
133
134    /**
135     *
136     * The handle function goal is to parse the matched syntax through the pattern function
137     * and to return the result for use in the renderer
138     * This result is always cached until the page is modified.
139     * @param string $match
140     * @param int $state
141     * @param int $pos
142     * @param Doku_Handler $handler
143     * @return array|bool
144     * @throws Exception
145     * @see DokuWiki_Syntax_Plugin::handle()
146     *
147     */
148    function handle($match, $state, $pos, Doku_Handler $handler)
149    {
150
151        switch ($state) {
152
153            case DOKU_LEXER_SPECIAL:
154            case DOKU_LEXER_ENTER:
155                // Get the parameters
156                $knownTypes = [];
157                $defaultAttributes = [];
158                $tagAttributes = TagAttributes::createFromTagMatch($match, $defaultAttributes, $knownTypes);
159                $callStack = CallStack::createFromHandler($handler);
160                $parent = $callStack->moveToParent();
161                $context = "";
162                if ($parent !== false) {
163                    $context = $parent->getTagName();
164                    if ($context === syntax_plugin_combo_link::TAG) {
165                        $context = $parent->getTagName();
166                    }
167                }
168                /**
169                 * Color setting should know the color of its parent
170                 * For now, we don't set any color if the parent is a button, note, link
171                 * As a header is not a parent, we may say that if the icon is contained, the default
172                 * branding color is not set ?
173                 */
174                $requestedColor = $tagAttributes->getValue(ColorRgb::COLOR);
175                if (
176                    $requestedColor === null &&
177                    Site::isBrandingColorInheritanceEnabled() &&
178                    !in_array($context, [
179                        syntax_plugin_combo_button::TAG,
180                        syntax_plugin_combo_note::TAG,
181                        syntax_plugin_combo_link::TAG
182                    ])
183                ) {
184                    $requestedWidth = $tagAttributes->getValue(Dimension::WIDTH_KEY, SvgDocument::DEFAULT_ICON_WIDTH);
185                    $requestedWidthInPx = Dimension::toPixelValue($requestedWidth);
186                    if ($requestedWidthInPx > 36) {
187                        // Illustrative icon
188                        $color = Site::getPrimaryColor();
189                    } else {
190                        // Character icon
191                        $color = Site::getSecondaryColor();
192                    }
193                    if ($color !== null) {
194                        $tagAttributes->setComponentAttributeValue(ColorRgb::COLOR, $color);
195                    }
196                }
197                return array(
198                    PluginUtility::STATE => $state,
199                    PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(),
200                    PluginUtility::CONTEXT => $context
201                );
202            case DOKU_LEXER_EXIT:
203                $callStack = CallStack::createFromHandler($handler);
204                $openingCall = $callStack->moveToPreviousCorrespondingOpeningCall();
205                return array(
206                    PluginUtility::STATE => $state,
207                    PluginUtility::ATTRIBUTES => $openingCall->getAttributes(),
208                    PluginUtility::CONTEXT => $openingCall->getContext()
209                );
210
211
212        }
213
214        return array();
215
216    }
217
218    /**
219     * Render the output
220     * @param string $format
221     * @param Doku_Renderer $renderer
222     * @param array $data - what the function handle() return'ed
223     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
224     * @see DokuWiki_Syntax_Plugin::render()
225     *
226     *
227     */
228    function render($format, Doku_Renderer $renderer, $data): bool
229    {
230
231        switch ($format) {
232
233            case 'xhtml':
234                {
235                    /** @var Doku_Renderer_xhtml $renderer */
236                    $state = $data[PluginUtility::STATE];
237                    switch ($state) {
238
239
240                        case DOKU_LEXER_SPECIAL:
241                            $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
242                            $renderer->doc .= $this->printIcon($tagAttributes);
243                            break;
244                        case DOKU_LEXER_ENTER:
245
246                            $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
247                            $tooltip = $tagAttributes->getValueAndRemoveIfPresent(\ComboStrap\Tooltip::TOOLTIP_ATTRIBUTE);
248                            if ($tooltip !== null) {
249                                /**
250                                 * If there is a tooltip, we need
251                                 * to start with a span to wrap the svg with it
252                                 */
253
254
255                                $tooltipTag = TagAttributes::createFromCallStackArray([\ComboStrap\Tooltip::TOOLTIP_ATTRIBUTE => $tooltip])
256                                    ->addClassName(syntax_plugin_combo_tooltip::TOOLTIP_CLASS_INLINE_BLOCK);
257                                $renderer->doc .= $tooltipTag->toHtmlEnterTag("span");
258                            }
259                            /**
260                             * Print the icon
261                             */
262                            $renderer->doc .= $this->printIcon($tagAttributes);
263                            /**
264                             * Close the span if we are in a tooltip context
265                             */
266                            if ($tooltip !== null) {
267                                $renderer->doc .= "</span>";
268                            }
269
270                            break;
271                        case DOKU_LEXER_EXIT:
272
273                            break;
274                    }
275
276                }
277                break;
278            case 'metadata':
279                /**
280                 * @var Doku_Renderer_metadata $renderer
281                 */
282                $tagAttribute = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
283                try {
284                    $name = $tagAttribute->getValueAndRemoveIfPresent("name");
285                    if ($name === null) {
286                        throw new ExceptionCombo("The attributes should have a name. It's mandatory for an icon.", self::CANONICAL);
287                    }
288                    $mediaPath = Icon::create($name, $tagAttribute)->getPath();
289                } catch (ExceptionCombo $e) {
290                    // error is already fired in the renderer
291                    return false;
292                }
293                if ($mediaPath instanceof DokuPath && FileSystems::exists($mediaPath)) {
294                    $mediaId = $mediaPath->getDokuwikiId();
295                    syntax_plugin_combo_media::registerFirstMedia($renderer, $mediaId);
296                }
297                break;
298
299        }
300        return true;
301    }
302
303    /**
304     * @param TagAttributes $tagAttributes
305     * @return string
306     */
307    private function printIcon(TagAttributes $tagAttributes): string
308    {
309        try {
310            $name = $tagAttributes->getValue("name");
311            if ($name === null) {
312                throw new ExceptionCombo("The attributes should have a name. It's mandatory for an icon.", self::CANONICAL);
313            }
314            return Icon::create($name, $tagAttributes)
315                ->render();
316        } catch (ExceptionCombo $e) {
317            return self::exceptionHandling($e, $tagAttributes);
318        }
319    }
320
321
322}
323