1<?php
2/**
3 * DokuWiki Syntax Plugin Combostrap.
4 *
5 */
6
7use ComboStrap\CallStack;
8use ComboStrap\ColorRgb;
9use ComboStrap\MarkupRef;
10use ComboStrap\PluginUtility;
11use ComboStrap\Shadow;
12use ComboStrap\Site;
13use ComboStrap\Skin;
14use ComboStrap\TagAttributes;
15use ComboStrap\TextColor;
16
17if (!defined('DOKU_INC')) {
18    die();
19}
20
21if (!defined('DOKU_PLUGIN')) {
22    define('DOKU_PLUGIN', DOKU_INC . 'lib/plugins/');
23}
24
25
26require_once(DOKU_PLUGIN . 'syntax.php');
27require_once(DOKU_INC . 'inc/parserutils.php');
28require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
29
30/**
31 * All DokuWiki plugins to extend the parser/rendering mechanism
32 * need to inherit from this class
33 *
34 * The name of the class must follow a pattern (don't change it)
35 * ie:
36 *    syntax_plugin_PluginName_ComponentName
37 *
38 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
39 * !!!!!!!!!!! The component name must be the name of the php file !!!
40 * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
41 *
42 * ===== For the Geek =====
43  * This is not the [[https://www.w3.org/TR/wai-aria-practices/#button|Button as describe by the Web Specification]]
44 * but a styling over a [[https://www.w3.org/TR/wai-aria-practices/#link|link]]
45 *
46 * ===== Documentation / Reference =====
47 * https://material.io/components/buttons
48 * https://getbootstrap.com/docs/4.5/components/buttons/
49 */
50class syntax_plugin_combo_button extends DokuWiki_Syntax_Plugin
51{
52
53
54    const TAG = "button";
55
56
57    /**
58     * Syntax Type.
59     *
60     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
61     * @see DokuWiki_Syntax_Plugin::getType()
62     */
63    function getType(): string
64    {
65        return 'formatting';
66    }
67
68    /**
69     * @return array
70     * Allow which kind of plugin inside
71     *
72     * No one of array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
73     * because we manage self the content and we call self the parser
74     */
75    public function getAllowedTypes(): array
76    {
77        return array('container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
78    }
79
80    public function accepts($mode): bool
81    {
82
83        return syntax_plugin_combo_preformatted::disablePreformatted($mode);
84
85    }
86
87    /**
88     * How Dokuwiki will add P element
89     *
90     *  * 'normal' - The plugin can be used inside paragraphs
91     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
92     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
93     *
94     * @see DokuWiki_Syntax_Plugin::getPType()
95     */
96    function getPType()
97    {
98        return 'normal';
99    }
100
101    /**
102     * @see Doku_Parser_Mode::getSort()
103     * the mode with the lowest sort number will win out
104     * the lowest in the tree must have the lowest sort number
105     * No idea why it must be low but inside a teaser, it will work
106     * https://www.dokuwiki.org/devel:parser#order_of_adding_modes_important
107     */
108    function getSort(): int
109    {
110        return 10;
111    }
112
113    /**
114     * Create a pattern that will called this plugin
115     *
116     * @param string $mode
117     * @see Doku_Parser_Mode::connectTo()
118     */
119    function connectTo($mode)
120    {
121
122        foreach (self::getTags() as $tag) {
123
124            $pattern = PluginUtility::getContainerTagPattern($tag);
125            $this->Lexer->addEntryPattern($pattern, $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
126
127        }
128
129    }
130
131    public function postConnect()
132    {
133
134        foreach (self::getTags() as $tag) {
135            $this->Lexer->addExitPattern('</' . $tag . '>', 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent());
136        }
137
138
139    }
140
141    /**
142     *
143     * The handle function goal is to parse the matched syntax through the pattern function
144     * and to return the result for use in the renderer
145     * This result is always cached until the page is modified.
146     * @param string $match
147     * @param int $state
148     * @param int $pos
149     * @param Doku_Handler $handler
150     * @return array|bool
151     * @throws Exception
152     * @see DokuWiki_Syntax_Plugin::handle()
153     *
154     */
155    function handle($match, $state, $pos, Doku_Handler $handler)
156    {
157
158        switch ($state) {
159
160            case DOKU_LEXER_ENTER:
161
162                $types = [ColorRgb::PRIMARY_VALUE, ColorRgb::SECONDARY_VALUE, "success", "danger", "warning", "info", "light", "dark"];
163                $defaultAttributes = array(
164                    Skin::SKIN_ATTRIBUTE => Skin::FILLED_VALUE,
165                    TagAttributes::TYPE_KEY => ColorRgb::PRIMARY_VALUE
166                );
167                $attributes = TagAttributes::createFromTagMatch($match, $defaultAttributes, $types);
168
169                /**
170                 * Note: Branding color (primary and secondary)
171                 * are set with the {@link Skin}
172                 */
173
174                /**
175                 * The parent
176                 * to apply automatically styling in a bar
177                 */
178                $callStack = CallStack::createFromHandler($handler);
179                $isInMenuBar = false;
180                while ($parent = $callStack->moveToParent()) {
181                    if ($parent->getTagName() === syntax_plugin_combo_menubar::TAG) {
182                        $isInMenuBar = true;
183                        break;
184                    }
185                }
186                if ($isInMenuBar) {
187                    if (!$attributes->hasAttribute("class") && !$attributes->hasAttribute("spacing")) {
188                        $attributes->addComponentAttributeValue("spacing", "mr-2 mb-2 mt-2 mb-lg-0 mt-lg-0");
189                    }
190                }
191
192                /**
193                 * The context give set if this is a button
194                 * or a link button
195                 * The context is checked in the `exit` state
196                 * Default context: This is not a link button
197                 */
198                $context = self::TAG;
199
200
201                return array(
202                    PluginUtility::STATE => $state,
203                    PluginUtility::ATTRIBUTES => $attributes->toCallStackArray(),
204                    PluginUtility::CONTEXT => $context
205                );
206
207            case DOKU_LEXER_UNMATCHED :
208
209                return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
210
211
212            case DOKU_LEXER_EXIT :
213                $callStack = CallStack::createFromHandler($handler);
214                $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall();
215                /**
216                 * Button or link button
217                 */
218                $context = self::TAG;
219                $descendant = $callStack->moveToFirstChildTag();
220                if ($descendant !== false) {
221                    if ($descendant->getTagName() === syntax_plugin_combo_link::TAG) {
222                        $context = syntax_plugin_combo_link::TAG;
223                    }
224                }
225                $openingTag->setContext($context);
226
227                return array(
228                    PluginUtility::STATE => $state,
229                    PluginUtility::CONTEXT => $context
230                );
231
232
233        }
234
235        return array();
236
237    }
238
239    /**
240     * Render the output
241     * @param string $format
242     * @param Doku_Renderer $renderer
243     * @param array $data - what the function handle() return'ed
244     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
245     * @see DokuWiki_Syntax_Plugin::render()
246     *
247     *
248     */
249    function render($format, Doku_Renderer $renderer, $data): bool
250    {
251
252        switch ($format) {
253
254            case 'xhtml':
255            {
256
257                /** @var Doku_Renderer_xhtml $renderer */
258
259                /**
260                 * CSS if dokuwiki class name for link
261                 */
262                if ($this->getConf(MarkupRef::CONF_USE_DOKUWIKI_CLASS_NAME, false)) {
263                    PluginUtility::getSnippetManager()->attachCssInternalStyleSheetForSlot(self::TAG);
264                }
265
266                /**
267                 * HTML
268                 */
269                $state = $data[PluginUtility::STATE];
270                $callStackAttributes = $data[PluginUtility::ATTRIBUTES];
271                $context = $data[PluginUtility::CONTEXT];
272                switch ($state) {
273
274                    case DOKU_LEXER_ENTER :
275
276                        /**
277                         * If this not a link button
278                         * The context is set on the handle exit
279                         */
280                        if ($context == self::TAG) {
281                            $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG)
282                                ->setDefaultStyleClassShouldBeAdded(false);
283                            self::processButtonAttributesToHtmlAttributes($tagAttributes);
284                            $tagAttributes->addOutputAttributeValue("type", "button");
285                            $renderer->doc .= $tagAttributes->toHtmlEnterTag('button');
286                        }
287                        break;
288
289                    case DOKU_LEXER_UNMATCHED:
290
291
292                        /**
293                         * If this is a button and not a link button
294                         */
295                        $renderer->doc .= PluginUtility::renderUnmatched($data);
296                        break;
297
298                    case DOKU_LEXER_EXIT :
299
300
301                        /**
302                         * If this is a button and not a link button
303                         */
304                        if ($context === self::TAG) {
305                            $renderer->doc .= '</button>';
306                        }
307
308                        break;
309                }
310                return true;
311            }
312
313        }
314        return false;
315    }
316
317
318    public static function getTags(): array
319    {
320        $elements[] = self::TAG;
321        $elements[] = 'btn';
322        return $elements;
323    }
324
325    /**
326     * @param TagAttributes $tagAttributes
327     */
328    public static function processButtonAttributesToHtmlAttributes(TagAttributes &$tagAttributes)
329    {
330        # A button
331        $btn = "btn";
332        $tagAttributes->addClassName($btn);
333
334        $type = $tagAttributes->getValue(TagAttributes::TYPE_KEY, "primary");
335        $skin = $tagAttributes->getValue(Skin::SKIN_ATTRIBUTE, Skin::FILLED_VALUE);
336        switch ($skin) {
337            case "contained":
338            {
339                $tagAttributes->addClassName("$btn-$type");
340                $tagAttributes->addComponentAttributeValue(Shadow::CANONICAL, true);
341                break;
342            }
343            case "filled":
344            {
345                $tagAttributes->addClassName("$btn-$type");
346                break;
347            }
348            case "outline":
349            {
350                $tagAttributes->addClassName("$btn-outline-$type");
351                break;
352            }
353            case "text":
354            {
355                $tagAttributes->addClassName("$btn-link");
356                $tagAttributes->addComponentAttributeValue(TextColor::TEXT_COLOR_ATTRIBUTE, $type);
357                break;
358            }
359        }
360
361
362        $sizeAttribute = "size";
363        if ($tagAttributes->hasComponentAttribute($sizeAttribute)) {
364            $size = $tagAttributes->getValueAndRemove($sizeAttribute);
365            switch ($size) {
366                case "lg":
367                case "large":
368                    $tagAttributes->addClassName("btn-lg");
369                    break;
370                case "sm":
371                case "small":
372                    $tagAttributes->addClassName("btn-sm");
373                    break;
374            }
375        }
376    }
377
378
379}
380