1<?php
2/**
3 * DokuWiki Syntax Plugin Combostrap.
4 *
5 */
6
7use ComboStrap\LogUtility;
8use ComboStrap\PluginUtility;
9use ComboStrap\Tag;
10use ComboStrap\TagAttributes;
11
12if (!defined('DOKU_INC')) {
13    die();
14}
15
16require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
17
18/**
19 *
20 * The name of the class must follow a pattern (don't change it)
21 * ie:
22 *    syntax_plugin_PluginName_ComponentName
23 */
24class syntax_plugin_combo_panel extends DokuWiki_Syntax_Plugin
25{
26
27    const TAG = 'panel';
28    const OLD_TAB_PANEL_TAG = 'tabpanel';
29    const STATE = 'state';
30    const SELECTED = 'selected';
31
32    /**
33     * When the panel is alone in the edit due to the sectioning
34     */
35    const CONTEXT_PREVIEW_ALONE = "preview_alone";
36    const CONTEXT_PREVIEW_ALONE_ATTRIBUTES = array(
37        self::SELECTED => true,
38        TagAttributes::ID_KEY => "alone",
39        TagAttributes::TYPE_KEY => syntax_plugin_combo_tabs::ENCLOSED_TABS_TYPE
40    );
41
42    const CONF_ENABLE_SECTION_EDITING = "panelEnableSectionEditing";
43
44    /**
45     * @var int a counter to give an id to the accordion panel
46     */
47    private $accordionCounter = 0;
48    private $tabCounter = 0;
49
50    private $sectionCounter = 0;
51
52    static function getSelectedValue(&$attributes)
53    {
54
55        if (isset($attributes[syntax_plugin_combo_panel::SELECTED])) {
56            /**
57             * Value may be false/true
58             */
59            $value = $attributes[syntax_plugin_combo_panel::SELECTED];
60            unset($attributes[syntax_plugin_combo_panel::SELECTED]);
61            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
62
63        }
64        if (isset($attributes[TagAttributes::TYPE_KEY])) {
65            if (strtolower($attributes[TagAttributes::TYPE_KEY]) === "selected") {
66                return true;
67            }
68        }
69        return false;
70
71    }
72
73    private static function getTags()
74    {
75        return [self::TAG, self::OLD_TAB_PANEL_TAG];
76    }
77
78
79    /**
80     * Syntax Type.
81     *
82     * Needs to return one of the mode types defined in $PARSER_MODES in parser.php
83     * @see DokuWiki_Syntax_Plugin::getType()
84     */
85    function getType()
86    {
87        return 'container';
88    }
89
90    /**
91     * @return array
92     * Allow which kind of plugin inside
93     * ************************
94     * This function has no effect because {@link SyntaxPlugin::accepts()} is used
95     * ************************
96     */
97    public function getAllowedTypes()
98    {
99        return array('container', 'base', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
100    }
101
102    public function accepts($mode)
103    {
104        /**
105         * header mode is disable to take over
106         * and replace it with {@link syntax_plugin_combo_heading}
107         */
108        if ($mode == "header") {
109            return false;
110        }
111        return syntax_plugin_combo_preformatted::disablePreformatted($mode);
112
113    }
114
115    /**
116     * How Dokuwiki will add P element
117     *
118     *  * 'normal' - The plugin can be used inside paragraphs
119     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
120     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
121     *
122     * @see DokuWiki_Syntax_Plugin::getPType()
123     */
124    function getPType()
125    {
126        return 'block';
127    }
128
129    /**
130     * @see Doku_Parser_Mode::getSort()
131     *
132     * the mode with the lowest sort number will win out
133     * the container (parent) must then have a lower number than the child
134     */
135    function getSort()
136    {
137        return 100;
138    }
139
140    /**
141     * Create a pattern that will called this plugin
142     *
143     * @param string $mode
144     * @see Doku_Parser_Mode::connectTo()
145     */
146    function connectTo($mode)
147    {
148
149        /**
150         * Only inside tabs and accordion
151         * and tabpanels for history
152         */
153        $show = in_array($mode,
154            [
155                PluginUtility::getModeFromTag(syntax_plugin_combo_tabs::TAG),
156                PluginUtility::getModeFromTag(syntax_plugin_combo_accordion::TAG),
157                PluginUtility::getModeFromTag(syntax_plugin_combo_tabpanels::TAG)
158            ]);
159
160        /**
161         * In preview, the panel may be alone
162         * due to the section edit button
163         */
164        if (!$show) {
165            global $ACT;
166            if ($ACT === "preview") {
167                $show = true;
168            }
169        }
170
171        /**
172         * Let's connect
173         */
174        if ($show) {
175            foreach (self::getTags() as $tag) {
176                $pattern = PluginUtility::getContainerTagPattern($tag);
177                $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
178            }
179        }
180
181    }
182
183    public function postConnect()
184    {
185
186        foreach (self::getTags() as $tag) {
187            $this->Lexer->addExitPattern('</' . $tag . '>', PluginUtility::getModeFromTag($this->getPluginComponent()));
188        }
189
190    }
191
192    /**
193     *
194     * The handle function goal is to parse the matched syntax through the pattern function
195     * and to return the result for use in the renderer
196     * This result is always cached until the page is modified.
197     * @param string $match
198     * @param int $state
199     * @param int $pos
200     * @param Doku_Handler $handler
201     * @return array|bool
202     * @throws Exception
203     * @see DokuWiki_Syntax_Plugin::handle()
204     *
205     */
206    function handle($match, $state, $pos, Doku_Handler $handler)
207    {
208
209        switch ($state) {
210
211            case DOKU_LEXER_ENTER:
212
213                // tagname to check if this is the old tag name one
214                $tagName = PluginUtility::getTag($match);
215
216                // Context
217                $tagAttributes = PluginUtility::getTagAttributes($match);
218                $tag = new Tag(self::TAG, $tagAttributes, $state, $handler);
219                $parent = $tag->getParent();
220                if ($parent != null) {
221                    $context = $parent->getName();
222                } else {
223                    /**
224                     * The panel may be alone in preview
225                     * due to the section edit button
226                     */
227                    global $ACT;
228                    if ($ACT == "preview") {
229                        $context = self::CONTEXT_PREVIEW_ALONE;
230                    } else {
231                        $context = $tagName;
232                    }
233                }
234
235
236                if (!isset($tagAttributes["id"])) {
237                    switch ($context) {
238                        case syntax_plugin_combo_accordion::TAG:
239                            $this->accordionCounter++;
240                            $id = $context . $this->accordionCounter;
241                            $tagAttributes["id"] = $id;
242                            break;
243                        case syntax_plugin_combo_tabs::TAG:
244                            $this->tabCounter++;
245                            $id = $context . $this->tabCounter;
246                            $tagAttributes["id"] = $id;
247                            break;
248                        case self::CONTEXT_PREVIEW_ALONE:
249                            $tagAttributes["id"] = "alone";
250                            break;
251                        default:
252                            LogUtility::msg("An id should be given for the context ($context)", LogUtility::LVL_MSG_ERROR, self::TAG);
253
254                    }
255                } else {
256
257                    $id = $tagAttributes["id"];
258                }
259
260                /**
261                 * Old deprecated syntax
262                 */
263                if ($tagName == self::OLD_TAB_PANEL_TAG) {
264
265                    $context = self::OLD_TAB_PANEL_TAG;
266
267                    $siblingTag = $parent->getPreviousSibling();
268                    if ($siblingTag != null) {
269                        if ($siblingTag->getName() === syntax_plugin_combo_tabs::TAG) {
270                            $descendants = $siblingTag->getDescendants();
271                            $tagAttributes[self::SELECTED] = false;
272                            foreach ($descendants as $descendant) {
273                                $descendantName = $descendant->getName();
274                                $descendantPanel = $descendant->getAttribute("panel");
275                                $descendantSelected = $descendant->getAttribute(self::SELECTED);
276                                if (
277                                    $descendantName == syntax_plugin_combo_tab::TAG
278                                    && $descendantPanel === $id
279                                    && $descendantSelected === "true") {
280                                    $tagAttributes[self::SELECTED] = true;
281                                    break;
282                                }
283                            }
284                        } else {
285                            LogUtility::msg("The direct element above a " . self::OLD_TAB_PANEL_TAG . " should be a `tabs` and not a `" . $siblingTag->getName() . "`", LogUtility::LVL_MSG_ERROR, "tabs");
286                        }
287                    }
288                }
289
290
291                return array(
292                    PluginUtility::STATE => $state,
293                    PluginUtility::ATTRIBUTES => $tagAttributes,
294                    PluginUtility::CONTEXT => $context,
295                    PluginUtility::POSITION => $pos
296                );
297
298            case DOKU_LEXER_UNMATCHED:
299
300                return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
301
302
303            case DOKU_LEXER_EXIT :
304
305                $tag = new Tag(self::TAG, array(), $state, $handler);
306                $openingTag = $tag->getOpeningTag();
307
308                /**
309                 * Label Mandatory check
310                 * (Only the presence of at minimum 1 and not the presence in each panel)
311                 */
312                if ($match != "</" . self::OLD_TAB_PANEL_TAG . ">") {
313                    $labelTag = $openingTag->getDescendant(syntax_plugin_combo_label::TAG);
314                    if (empty($labelTag)) {
315                        LogUtility::msg("No label was found in the panel (number " . $this->tabCounter . "). They are mandatory to create tabs or accordion", LogUtility::LVL_MSG_ERROR, self::TAG);
316                    }
317                }
318
319                /**
320                 * Section
321                 * +1 to go at the line
322                 */
323                $endPosition = $pos + strlen($match) + 1;
324
325                return
326                    array(
327                        PluginUtility::STATE => $state,
328                        PluginUtility::CONTEXT => $openingTag->getContext(),
329                        PluginUtility::POSITION => $endPosition
330                    );
331
332
333        }
334
335        return array();
336
337    }
338
339    /**
340     * Render the output
341     * @param string $format
342     * @param Doku_Renderer $renderer
343     * @param array $data - what the function handle() return'ed
344     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
345     * @see DokuWiki_Syntax_Plugin::render()
346     *
347     *
348     */
349    function render($format, Doku_Renderer $renderer, $data)
350    {
351
352        if ($format == 'xhtml') {
353
354            /** @var Doku_Renderer_xhtml $renderer */
355            $state = $data[PluginUtility::STATE];
356            switch ($state) {
357
358                case DOKU_LEXER_ENTER :
359
360                    /**
361                     * Section (Edit button)
362                     */
363                    if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
364                        $position = $data[PluginUtility::POSITION];
365                        $this->sectionCounter++;
366                        $name = "section" . self::TAG . $this->sectionCounter;
367                        PluginUtility::startSection($renderer, $position, $name);
368                    }
369
370                    $context = $data[PluginUtility::CONTEXT];
371                    switch ($context) {
372                        case syntax_plugin_combo_accordion::TAG:
373                            // A panel in a accordion
374                            $renderer->doc .= "<div class=\"card\">";
375                            break;
376                        case self::OLD_TAB_PANEL_TAG: // Old deprecated syntax
377                        case syntax_plugin_combo_tabs::TAG: // new syntax
378
379                            $attributes = $data[PluginUtility::ATTRIBUTES];
380
381                            PluginUtility::addClass2Attributes("tab-pane fade", $attributes);
382
383                            $selected = self::getSelectedValue($attributes);
384                            if ($selected) {
385                                PluginUtility::addClass2Attributes("show active", $attributes);
386                            }
387                            unset($attributes[self::SELECTED]);
388
389                            $attributes["role"] = "tabpanel";
390                            $attributes["aria-labelledby"] = $attributes["id"] . "-tab";
391
392                            $renderer->doc .= '<div ' . PluginUtility::array2HTMLAttributesAsString($attributes) . '>';
393                            break;
394                        case self::CONTEXT_PREVIEW_ALONE:
395                            $aloneAttributes = syntax_plugin_combo_panel::CONTEXT_PREVIEW_ALONE_ATTRIBUTES;
396                            $renderer->doc .= syntax_plugin_combo_tabs::openTabPanelsElement($aloneAttributes);
397                            break;
398                        default:
399                            LogUtility::log2FrontEnd("The context ($context) is unknown in enter rendering", LogUtility::LVL_MSG_ERROR, self::TAG);
400                            break;
401                    }
402
403                    break;
404                case DOKU_LEXER_EXIT :
405                    $context = $data[PluginUtility::CONTEXT];
406                    switch ($context) {
407                        case syntax_plugin_combo_accordion::TAG:
408                            $renderer->doc .= '</div>' . DOKU_LF . "</div>" . DOKU_LF;
409                            break;
410                        case self::CONTEXT_PREVIEW_ALONE:
411                            $aloneVariable = syntax_plugin_combo_panel::CONTEXT_PREVIEW_ALONE_ATTRIBUTES;
412                            $renderer->doc .= syntax_plugin_combo_tabs::closeTabPanelsElement($aloneVariable);
413                            break;
414
415                    }
416
417                    /**
418                     * End section
419                     */
420                    if (PluginUtility::getConfValue(self::CONF_ENABLE_SECTION_EDITING, 1)) {
421                        $renderer->finishSectionEdit($data[PluginUtility::POSITION]);
422                    }
423
424                    /**
425                     * End panel
426                     */
427                    $renderer->doc .= "</div>" . DOKU_LF;
428                    break;
429                case DOKU_LEXER_UNMATCHED:
430                    $renderer->doc .= PluginUtility::renderUnmatched($data);
431                    break;
432            }
433            return true;
434        }
435        return false;
436    }
437
438
439}
440